Rozsáhlou část v teorii programování tvoří problematika chyb:
Chyby v programu jsou obecně dvojího druhu:
Chyby odhalené při překladu (často syntaktické chyby) jsou z hlediska uživatele, který naši appku používá, příjemnější. Překladač (např. překladač jazyka C#) nedovolí kód spustit (ani ho nepřeloží, ani nesestaví tzv. assembly), dokud vývojář chybu neodstraní. K uživateli jeho aplikace se tedy "naštěstí" taková chyba "neprodere".
Chyby vzniklé za běhu programu mají přímý dopad na jeho uživatele. Programátor musí vymyslet, co si s takovou chybou počít. (Pokud tedy vůbec v té části kódu, kde k chybě došlo, s možností výskytu chyby počítal.)
Pro zpracování očekávatelných run-time chyb slouží výjimky.
Představme si metodu pro výpočet faktoriálu. Faktoriál je funkce nad přirozenými čísly, která vezme dané přirozené číslo a k tomu všechna ostatní přirozená čísla, které jsou menší než dané číslo, a všechna ta čísla mezi sebou pronásobí. Značí se vykřičníkem (!). Např. 3! = 3 * 2 * 1 = 6.
Pro nulu definujeme: 0! = 1
Z logiky věci vyplývá, že faktoriál libovolného přirozeného čísla musí vyjít vždy kladný.
Než se v programování prosadila koncepce výjimek, vracely se volajícímu chybové stavy prostřednictvím návratové hodnoty.
public class Program { public static void Main(string[] args) { Console.Write("Zadejte přirozené číslo: "); int a = Convert.ToInt32(Console.ReadLine()); int aFact = CalculateFactorial(a); if (aFact > 0) { Console.WriteLine($"Faktoriál čísla {a} je {aFact}."); } else { Console.WriteLine($"Faktoriál čísla {a} nelze vypočítat. Nejspíše jste zadali celé číslo, které není přirozené."); } } private static int CalculateFactorial(int n) { if (n < 0) { return -1; } else if (n == 0) { return 1; } else { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } } }
Z příkladu zdrojového kódu výše je vidět, že to, co nám vrátí metoda CalculateFactorial
a co se v hlavní metodě Main
pak uloží do proměnné aFact lze v různých situacích
interpretovat dvěma různými způsoby:
CalculateFactorial
) kladné číslo nebo nula, funkce CalculateFactorial
nám vrátí hodnotu faktoriálu pro takové číslo a my výsledek vypíšeme na konzoli.CalculateFactorial
předáme záporné číslo, funkce nám vrátí rovněž záporné číslo (-1) a my musíme v takovém případě vypsat na konzoli hlášení, že faktoriál nelze vypočítat.Toto ale není dobře. V proměnné aFact máme jednou platnou hodnotu faktoriálu, jindy indikaci chybového stavu. Použitím výjimek tuto nekonzistenci ve významu proměnné aFact odstraníme.
Výjimka je v běžícím programu prostředek, jak dát najevo aktuálně vykonávanému bloku, případně blokům, které tento blok
vyvolaly (např. provádíme funkci FuncA(...)
zavolanou z procedury ProcB(...)
,
kterou jsme zavolali někde z hlavní metody Main(...)
), že došlo k nějakému neočekávanému (= výjimečnému) stavu
(zkrátka se vyskytla chyba), aniž bychom chybový kód museli předávat ve výsledku volání, jak tomu bylo dříve.
Obecně může dojít během provádění programu k více typům výjimečných stavů, na jednom nebo i na více místech. Program by se měl k výskytu výjimky nějak postavit. Na místě, kde k výjimečnému stavu dojde, se výjimka tzv. vyhodí ("throw"). Na místě, kde se výjimečný stav řeší nebo se s ním nějak naloží, se výjimka tzv. chytí ("catch"). Někdy se setkáme s pojmem strukturovaná obsluha výjimek, protože je možné se ke každému typu chyby postavit jinak, jak uvidíme dále.
V C# vypadá syntaxe programové konstrukce pro zachycení výjimky takto:
try { blok-potenciálního-výskytu-výjimky } catch (typ-výjimky proměnná-výjimky) { blok-obsluhy-výjimky } finally { blok-důležitého-kódu }
Příkazy v bloku-potenciálního-výskytu-výjimky v části try konstrukce try-catch-finally jsou příkazy, které se mají provést. To je ta výkonná část kódu, kterou programujeme. Jen dopředu nevíme, zda "to dopadne dobře", zda se podaří celý blok vykonat. Chceme to "zkusit", proto "try".
Pokud to "dopadne dobře", provedou se všechny příkazy v bloku-potenciálního-výskytu-výjimky v části try, potom se provedou příkazy v bloku-důležitého-kódu v části finally konstrukce try-catch-finally a dále se pokračuje prvním příkazem následujícím za touto try-catch-finally konstrukcí.
Pokud dojde během provádění programu v bloku-potenciálního-výskytu-výjimky k nějakému výjimečnému stavu, tedy pokud se v něm skutečně vyskytne běhová chyba, provádění bloku-potenciálního-výskytu-výjimky se přeruší (ostatní příkazy v tomto bloku try za místem výskytu chyby se už nevykonají) a řízení se předá do bloku-obsluhy-výjimky v části catch konstrukce try-catch-finally.
V bloku-obsluhy-výjimky bychom měli nějak s výjimkou naložit: Vypsat ji na konzoli, zalogovat ji apod. Po vykonání této části catch konstrukce try-catch-finally se ještě vykonají příkazy v bloku-důležitého-kódu v části finally konstrukce try-catch-finally. Dále se pokračuje prvním příkazem za celou touto konstrukcí.
Část finally je nepovinná. Stačí i konstrukce try-catch bez finally. Jelikož v části try se může stát, že při výskytu chyby se vykonávání bloku-potenciálního-výskytu-výjimky nedokončí, znamená to, že některé příkazy z tohoto bloku se nikdy neprovedou. Část finally se používá v situacích, kdy potřebujeme, aby se něco provedlo bez ohledu na to, zda k výjimce došlo, nebo ne.
Nyní přepíšeme program na výpočet faktoriálu tak, že místo chybového kódu v návratové hodnotě použijeme strukturovanou obsluhu výjimek:
public class Program { public static void Main(string[] args) { Console.Write("Zadejte přirozené číslo: "); int a = Convert.ToInt32(Console.ReadLine()); int aFact; try { aFact = CalculateFactorial(a); Console.WriteLine($"Faktoriál čísla {a} je {aFact}."); } // Zachycení výjimky. catch (Exception ex) { // Zpracování výjimky. Console.WriteLine($"Faktoriál čísla {a} nelze vypočítat. Nejspíše jste zadali celé číslo, které není přirozené."); Console.WriteLine("Text chyby:"); Console.WriteLine(ex.Message); } } private static int CalculateFactorial(int n) { if (n < 0) { // Vyhození výjimky. throw new Exception($"CalculateFactorial: The argument must be a non-negative integer: {n}"); } else if (n == 0) { return 1; } else { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } } }
Z příkladu je vidět, že v části catch v bloku-obsluhy-výjimky můžeme použít proměnnou-výjimky,
což je reference na objekt se zachycenou výjimkou. Přitom typ-výjimky
by měl odpovídat datovému typu výjimky, která byla vyhozena (viz řádek throw...
v metodě CalculateFactorial).
Obecně totiž může být konstrukce try-catch-finally složitější v tom, že částí catch může obsahovat více než jednu. Syntaxe:
try { blok-potenciálního-výskytu-výjimky } catch (typ-speciálnější-výjimky proměnná-speciálnější-výjimky) { blok-obsluhy-speciálnější-výjimky } catch (typ-obecnější-výjimky proměnná-obecnější-výjimky) { blok-obsluhy-obecnější-výjimky } catch (typ-ještě-obecnější-výjimky proměnná-ještě-obecnější-výjimky) { blok-obsluhy-ještě-obecnější-výjimky } // ... finally { blok-důležitého-kódu }
Výjimky mají (jako všechny objekty v .NETu) své typy, které tvoří objektovou hierarchii. Když dojde k výjimce, dispečer obsluhy výjimek hledá postupně v částech catch typ výjimky, který by byl kompatibilní s typem výjimky, která byla právě vyhozena.
Jako příklad objektové hierarchie výjimek vezměme třeba tento:
Exception
ArgumentException
ArgumentNullException
Typ ArgumentException
je speciálnějším případem typu Exception
.
A typ ArgumentNullException
je zase speciálnějším případem typu ArgumentException
.
Části catch musí být seřazeny od méně obecných typů výjimek k obecnějším. Když dispečer obsluhy výjimek narazí na část catch s typem výjimky, který je kompatibilní s aktuálně vyhozenou výjimkou, provede její blok-obsluhy-výjimky a další části catch již neprohledává. Poté pokračuje blokem-důležitého-kódu (pokud je přítomna část finally) a kódem, který následuje za celou konstrukcí try-catch-finally (případně try-catch).
Proto je vhodné dát ke speciálnějším typům výjimek speciálnější kód, který s nimi nějak naloží.
S obecnou výjimkou (vrchol hierarchie výjimek – typ Exception
) většinou uděláme
jen to, že ji např. zapíšeme do logu. Z jejího typu totiž moc nevyčteme, co se vlastně stalo.
Příklad strukturované obsluhy výjimek při zápisu do textového souboru. Dejme tomu, že máme obecnou metodu pro logování, které předáme název logovacího souboru a řádek textu, který má být do logu zapsán:
using System; using System.IO; public class Logger { public void LogToFile(string logFile, string message) { StreamWriter sw = null; try { // Open an existing file, append the given message to the end of it, then close the file. sw = new StreamWriter(logFile, "true"); sw.WriteLine(message); sw.Close(); sw = null; } // Exception ---> UnauthorizedAccessException catch (UnauthorizedAccessException ex1) { Console.WriteLine($"LogToFile: Access is denied: {logFile}"); Console.WriteLine($"Exception message: {ex1.Message}"); } // Exception ---> SecurityException catch (SecurityException ex2) { Console.WriteLine($"LogToFile: The caller does not have the required permission: {logFile}"); Console.WriteLine($"Exception message: {ex2.Message}"); } // Exception ---> IOException ---> DirectoryNotFoundException catch (DirectoryNotFoundException ex3) { Console.WriteLine($"LogToFile: The specified path is invalid: {logFile}"); Console.WriteLine($"Exception message: {ex3.Message}"); } // Exception ---> IOException catch (IOException ex4) { Console.WriteLine("LogToFile: An error occurred during an I/O operation."); Console.WriteLine($"Exception message: {ex4.Message}"); } // Exception catch (Exception ex5) { Console.WriteLine("LogToFile: A general error occurred."); Console.WriteLine($"Exception message: {ex5.Message}"); } finally { if (sw != null) { // The file is probably still open. Try to close it. sw.Close(); sw = null; } } } }
Protože hierarchie výjimek v .NETu v knihovnách, které pracují se soubory a souborovým systémem, vypadá nějak takto:
Exception
IOException
DirectoryNotFoundException
SecurityException
UnauthorizedAccessException
Musí být části catch v konstrukci try-catch-finally seřazené tak,
že část catch s typem výjimky Exception
musí následovat až
po všech ostatních catch, kde jsou výjimky speciálnější.
A dále část catch s typem výjimky IOException
musí
následovat až po části catch s výjimkou DirectoryNotFoundException
.