Hodně programátorů, kteří chtějí psát své programy čistě, se občas zabývají otázkou, jak nejlépe ošetřovat chyby, aby kód byl stále přehledný. Drtivá většina API funkcí Windows buďto vrací chybu, nebo ji umožňuje zjistit pomocí volání funkce GetLastError(). Tento článek shrnuje moje zkušenosti s ošetřováním chyb při volání API funkcí. Rád bych zdůraznil, že vhodnost nebo nevhodnost jednotlivých možností uvedených v tomto článku mohou být subjektivní, a nemusí s nimi každý souhlasit.
Ošetřování chyb si vyzkoušíme na jednoduchém příkladu kopírování souboru. Za předpokladu, že nechceme použít funkci CopyFile(), která provede vše v jednom kroku, musíme provést následující
V bodech 1-3 by mohla nastat chyba, která způsobí, že kopírování nebude dokončeno v pořádku. V případě výskytu chyb by měl program korektně reagovat, uvolnit alokované prostředky a oznámit chybu uživateli.
Podívejme se, jak by takový postup vypadal, kdybychom nemuseli ošetřovat chyby:
HANDLE hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); HANDLE hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); VOID * lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize); while(dwTransferred != 0) { ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL); if(dwTransferred != 0) WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL); } HeapFree(GetProcessHeap(), 0, lpBuffer); CloseHandle(hFileTrg); CloseHandle(hFileSrc);
Program je relativně krátký, a (snad) i čitelný. Bohužel ne vždy funkční. Nikdo nám nezaručí, že zdrojový soubor půjde otevřít. Nevíme, zda existuje, a i když existuje, může být chráněn proti otevření. Stejně tak nevíme, zda se nám podaří vytvořit cílový soubor. A alokace paměti může selhat, pokud se např. pokusíme alokovat příliš velký kus, nebo pokud je paměti celkově málo (i když v takovém případě se nejspíš zbortí celý systém).
První metoda ošetření chyb spočívá v uvolnění alokovaných prostředků bezprostředně po zjištění chyby a v následném návratu z funkce:
// Otevřeme zdrojový soubor HANDLE hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if(hFileSrc == INVALID_HANDLE_VALUE) return GetLastError(); // Otevřeme cílový soubor HANDLE hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if(hFileTrg == INVALID_HANDLE_VALUE) { nError = GetLastError(); CloseHandle(hFileSrc); return nError; } // Alokujeme paměť VOID * lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize); if(lpBuffer == NULL) { nError = GetLastError(); CloseHandle(hFileTrg); CloseHandle(hFileSrc); return nError; } // Překopírujeme obsah souboru while(dwTransferred != 0) { ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL); if(dwTransferred != 0) WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL); } // Uvolníme alokované prostředky a návrat HeapFree(GetProcessHeap(), 0, lpBuffer); CloseHandle(hFileTrg); CloseHandle(hFileSrc);
Tato metoda je provedena čistě, žádná chyba nezůstane neošetřena. Nepříjemné je ale to, že v kódu na několik místech voláme uzavírání souborů, které nesmíme za žádnou potenciálně chybnou operací zapomenout. Pokud ještě navíc při udržování kódu vložíme další mezikrok, budeme muset přidat uvolňování prostředků i za tento mezikrok (pokud skončí s chybou). Navíc do každého dalšího kroku musíme vložit ještě uvolňování prostředků vzniklých tímto mezikrokem.
Můžeme také napsat kód tak, aby byl další krok proveden jedině v případě, kdy se nepodaří provést krok předchozí. Kód může vypadat třeba takto:
// Otevřeme zdrojový soubor HANDLE hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if(hFileSrc != INVALID_HANDLE_VALUE) { // Otevřeme cílový soubor HANDLE hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if(hFileTrg != INVALID_HANDLE_VALUE) { // Alokujeme paměť VOID * lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize); if(lpBuffer != NULL) { DWORD dwTransferred = 1; // Překopírujeme obsah souboru while(dwTransferred != 0) { ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL); if(dwTransferred != 0) WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL); } // Uvolníme paměť HeapFree(GetProcessHeap(), 0, lpBuffer); } else nError = GetLastError(); // Uzavřeme cílový soubor CloseHandle(hFileTrg); } else nError = GetLastError(); // Uzavřeme zdrojový soubor CloseHandle(hFileSrc); } else nError = GetLastError();
Dobrá metoda, žádné volání tam není navíc. Kód vypadá přehledně. Pokud by ale měl počet kroků být větší (10 nebo více), tak se odsazení nejvíce vnořených částí pohybuje v polovině používaných editorů (pokud odsazujete - jako já - po čtyřech mezerách). Na samotný zdrojový text už zbývá poměrně málo místa (pokud ovšem nepoužíváte rozlišení 1600 na 1200, kdy jsou písmenka tak maličká, že nejsou skoro poznat :-)).
K ošetření chyb je možné využít i strukturovaných vyjímek - např. pomocí bloku try-finally. Pokud blok try jakýmkoliv způsobem opustíme, vždy se nejdříve spustí ještě blok finally. V tomto bloku provedeme úklid. Abychom ale poznali, kterou proměnnou je třeba "uklidit", musíme na začátku funkce všechny použité proměnné initializovat na neplatnou hodnotu - ukazatele na NULL, handly na NULL nebo na hodnotu INVALID_HANDLE_VALUE:
HANDLE hFileSrc = INVALID_HANDLE_VALUE; HANDLE hFileTrg = INVALID_HANDLE_VALUE; LPVOID lpBuffer = NULL; __try { // Otevřeme zdrojový soubor hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if(hFileSrc == INVALID_HANDLE_VALUE) return GetLastError(); // Otevřeme cílový soubor hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if(hFileTrg == INVALID_HANDLE_VALUE) return GetLastError(); // Alokujeme paměť lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize); if(lpBuffer == NULL) return GetLastError(); // Překopírujeme obsah souboru while(dwTransferred != 0) { ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL); if(dwTransferred != 0) WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL); } } __finally { // Uvolníme prostředky if(lpBuffer != NULL) HeapFree(GetProcessHeap(), 0, lpBuffer); if(hFileTrg != INVALID_HANDLE_VALUE) CloseHandle(hFileTrg); if(hFileSrc != INVALID_HANDLE_VALUE) CloseHandle(hFileSrc); } return ERROR_SUCCESS;
Poslední zde uvedená metoda je podobná té předchozí, jenom nepoužívá block try-except. Každý krok se provede pouze tehdy, když všechny předchozí skončily úspěšně.
HANDLE hFileSrc = INVALID_HANDLE_VALUE; HANDLE hFileTrg = INVALID_HANDLE_VALUE; LPVOID lpBuffer = NULL; int nError = ERROR_SUCCESS; // Otevřeme zdrojový soubor if(nError == ERROR_SUCCESS) { hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if(hFileSrc == INVALID_HANDLE_VALUE) nError = GetLastError(); } // Otevřeme cílový soubor if(nError == ERROR_SUCCESS) { hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if(hFileTrg == INVALID_HANDLE_VALUE) nError = GetLastError(); } // Alokujeme paměť if(nError == ERROR_SUCCESS) { lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize); if(lpBuffer == NULL) nError = GetLastError(); } // Překopírujeme obsah souboru if(nError == ERROR_SUCCESS) { while(dwTransferred != 0) { ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL); if(dwTransferred != 0) WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL); } } // Uvolníme prostředky if(lpBuffer != NULL) HeapFree(GetProcessHeap(), 0, lpBuffer); if(hFileTrg != NULL) CloseHandle(hFileTrg); if(hFileSrc != NULL) CloseHandle(hFileSrc); return nError;
Uvedené metody ošetřování chyb berte pouze jako tip, jak ošetřovat chyby. Z praxe vím, že každý programátor to dělá po svém - většinou začíná s první metodou, ale brzy zjistí, že moc nevyhovuje, a tak na chyby vracené API funkcemi nějak reaguje. Pokud budete programovat profesionální aplikaci většího rozsahu, která by měla být "neshoditelná", tedy velice stabilní, většinou se toho lépe dosahuje s aplikací, která důsledně testuje vracené chyby. Nepředpokládejte, že nějaká funkce vždy skončí s úspěchem - vždy je možné, že soubor, klíč registru nebo adresář nelze otevřít/vytvořit, nebo že nemáme k dispozici dostatek paměti.
Copyright Ladislav Zezula 11.09.2003