Zpracování chyb a výjimek
4. Ošetření více výjimek
V předchozích kapitolách bylo naznačeno, že existuje více druhů výjimek (tzn. více druhů chyb, které mohou nastat při provádění programu. Proto v C# lze definovat více handlerů pro specifické typy výjimek. Syntaxe použití více handlerů např. 2 handlery je následující:
try
{
nebezpečnýPříkaz
}
catch(typ1 identifikátorVýjimky1)
{
těloHandleru1
}
catch(typ2 identifikátorVýjimky2)
{
těloHandleru2
} kde:
- nebezpečnýPříkaz - představuje příkaz nebo složený příkaz, který obsahuje potenciální nebezpečí chyby. Když dojde ke spuštění tohoto bloku a nedojde k vyvolání výjimky (nedojde k žádné chybě), proběhnou všechny příkazy postupně
- těloHandleru1 - pokud dojde k chybě při provádění nebezpečného příkazu, provádění kódu v tomto nebezpečném příkazu se opustí a skočí se do příslušného bloku catch na základě typu výjimky. Pokud tedy nastala výjimka typu typ1 je generován objekt identifikátorVýjimky1 a výjimka bude ošetřena pomocí těloHandleru1
Pokud kód v prvním handleru vygeneroval další výjimku, nebude hledán příslušný handler v sousedních handlerech dané metody, ve které nastala výjimka (i když tam je příslušný handler definován), ale bude hledán příslušný handler ve volající metodě (kaskádování) – princip, který jsme ukazovali již v předchozí kapitole Neošetřené výjimky.
Mechanismus ošetření výjimek v C# je velmi komplexní a zachycuje většinu možných výjimek, které mohou nastat. Jak ale zajistit ošetření všech možných výjimek, které jsme ani při psaní kódu neuvažovali? Výjimky jsou ve vzájemných vztazích. Výjimky jsou organizovány do rodin nazývajících se hierarchie dědičnosti např. FormatException a OverflowException patří do rodiny zvané SystemException, stejně jako řada dalších výjimek. Výjimka SystemException je sama členem širší rodiny jednoduše nazvané Exception, která je pradědečkem všech výjimek. Pokud tedy ošetříme výjimku typu Exception, zachytíme tím všechny možné výjimky, které mohou být vyvolány. Rodina výjimek zahrnuje celou řadu výjimek, z nichž mnohé jsou určeny pro použití různými částmi rozhraní .NET Framework. Některé z těchto výjimek jsou poněkud ezoterické, ale je užitečné pochopit, jak je ošetřit. [6]
Asi si říkáte, co se stane, když výjimka bude vyhovovat dvou handlerům např. pro výjimku typu FormatException a obecný typ Exception. V tomto případě bude spuštěn první nalezený handler, který vyhovuje typu vyvolané výjimky. Všechny ostatní handlery budou ignorovány. Pokud tedy na začátek obsluh výjimek specifikujeme obecnou výjimku typu Exception ostatní obsluha výjimek nebude spuštěna. Proto je vhodné na začátek bloků s catch umísťovat specifické handlery a za nimi ty obecnější.
Ilustrativní příklad KalkulackaTypyVyjimek
Program KalkulackaTypyVyjimek
- Popis: Ošetření více typů výjimek
- Zadání: Zkuste v hlavní statické metodě Main v programu KalkulackaVicenasobneVetveni_Main implementovat pomocí příkazů try, catch ošetření různých typů výjimek. Aplikujte ošetření try na potřebný zdrojový kód. Dané výjimky otestujte. Jedná se o následující typy výjimek:
- DivideByZeroException - dělení nulou
- FormatException - chyba při konverzi
- Exception - obecná výjimka
- Řešení:
KalkulackaTypyVyjimek_Main
Vraťme se k našemu
příkladu kalkulačky. Pro vyzkoušení obsluhy výjimek použijeme verzi programu KalkulackaVicenasobneVetveni_Main s vícenásobným
větvením. Potenciálně nebezpečný kód jsme zavřeli do těla příkazu try – viz Kód 1. Prvním
problémem, který může nastat, je chyba na vstupu, to znamená, že uživatel zadá
namísto číslice nějaká písmena nebo jiné znaky. Pak by došlo k chybě při
konverzi načteného textu do podoby čísla, která by vyvolala výjimku FormatException. Tento problém může
nastat u příkazů Parse u typu int a double.
static void Main(string[] args) { /*Deklarace proměnných: 'kod' ... celočíselný kód aritmetické operace, * 'a' ... první zadané číslo, 'b' ... druhé zadané číslo; * 'c' ... výsledek aritmetické operace*/ int kod; double a, b, c; //Výpis výzvy k zadání operace Console.Write("Zadejte kód operace (1 ... sčítání, 2 ... odečítání, " + "3 ... násobení, 4 ... dělení): "); //Potenciálně nebezpečný kód try { //Načtení kódu operace kod = int.Parse(Console.ReadLine()); //Výpis výzvy k zadání prvního čísla Console.Write("Zadejte A: "); //Načtení prvního zadaného čísla a = double.Parse(Console.ReadLine()); //Výpis výzvy k zadání druhého čísla Console.Write("Zadejte B: "); //Načtení druhého zadaného čísla b = double.Parse(Console.ReadLine()); //Přepnutí do příslušné sekce podle hodnoty 'kod' switch (kod) { //Operace sečtení case 1: //Sečtení dvou zadaných čísel c = a + b; //Ukonnčení sekce pro kod == 1 break; //Operace odečtení case 2: //Odečtení dvou zadaných čísel c = a - b; break; //Operace násobení case 3: //Násobení dvou zadaných čísel c = a * b; break; //Operace dělení case 4: //Podíl dvou zadaných čísel c = a / b; break; //Zadání špatného číselného kódu operace default: //Výpis chyby v zadávání kódu Console.WriteLine("Zadali jste špatný číselný kód!"); //Zamezení provádění dalších příkazů - opuštění metody return; } //Výpis textu s hodnotou výsledku Console.WriteLine("Výsledek C: {0}", c); }
Kód 1: První výřez kódu těla hlavní metody KalkulackaTypyVyjimek_Main – použití příkazu try
Druhá část kódu obsahuje různé handlery (bloky s obsluhou výjimek pomocí příkazů catch ať už s typem výjimky a objektem, nebo bez). Tyto handlery následují po ukončení těla příkazu try (za koncem složené závorky) – viz Kód 2. Je zde uvedeno ošetření možných vyvolaných výjimek – pro případy:
- Dělení nulou – typ výjimky DivideByZeroException
- Chyby při konverzi textového řetězce na číslo, ať už celé číslo (u proměnné kod), tak na reálné číslo (proměnná a, b) pomocí příkazu Parse – typ výjimky FormatException
- Nebo jiné výjimky, které mohou nastat – typ výjimky Exception. S použitím této výjimky buďte opatrní. Je to prapředek všech výjimek, takže jakákoliv jiná výjimka, která nebyla uvedena před touto obecnou (vyjma předchozích dvou), bude spadat do tohoto handleru. U tohoto handleru nemusí být uvedena ani kulatá závorka s typem a identifikátorem výjimky. Stačí pouze napsat catch a tělo bloku kódu ošetřujícího výjimku. Tím ale bohužel zároveň ztrácíme informaci o příčině chyby
//Handler pro dělení nulou bez parametru handleru catch (DivideByZeroException) { //Výpis chyby Console.WriteLine("B nesmí být 0"); //Zamezení provádění dalších příkazů - opuštění metody return; } //Handler chyby při konverzi s parametrem handleru catch (FormatException exFormat) { //Výpis parametru handleru Console.WriteLine("Chyba při konverzi: {0}", exFormat.ToString()); } //Obecná výjimka typu Exception (stačilo by pouze catch) – používat obezřetně catch (Exception ex) { Console.WriteLine("Nastala jiná výjimka: {0}", ex.ToString()); } //Čekání na stisk klávesy -> Ukončení programu Console.ReadKey(); }
Kód 2: Druhá část kódu těla hlavní metody KalkulackaTypyVyjimek_Main – použití různých typů výjimek
V následujícím výpisu
v konzoli vztahující se k našemu programu Kalkulacka je uveden příklad operace dělení čísla 5 a 2 – viz Výstup programu 1. Naše
aplikace tedy funguje, pokud neuděláme nějakou chybu. To se ale děje jen
v ideálním případě. Pojďme tedy funkčnost aplikace trochu otestovat.
Zadejte kód operace (1 ... sčítání, 2 ... odečítání, 3 ... násobení, 4 ... dělení): 4
Zadejte A: 5
Zadejte B: 2
Výsledek C: 2,5
Výstup programu 1 - Kód KalkulackaTypyVyjimek_Main: Ukázka výsledku podílu
V následujícím
výpisu v konzoli vidíme, co se stane, když uživatel namísto zadání celého
čísla zadá text „bflm“ – viz Výstup programu 2. Tento
text se jen těžko dá konvertovat do podoby celého čísla. Proto byl vykonán
handler s typem výjimky FormatException.
Poněvadž jsme si definovali i objekt odvozený od této třídy výjimky, máme
možnost si nechat vypsat (pomocí příkazu ToString())
příčinu selhání našeho programu. Z vypsaného textu chyby lze zjistit, vstupní
textový řetězec nebyl ve správném formátu (v případě, že jste si nainstalovali českou sadu ve Visual Studio: Vstupní řetězec nemá správný formát . Pokud máte anglickou lokaci výpis výjimky vypadá asi takto: Input
string was not in a correct format) atd.
Zadejte kód operace (1 ... sčítání, 2 ... odečítání, 3 ... násobení, 4 ... dělení): 4
Zadejte A: bflm
Chyba při konverzi: System.FormatException: Vstupní řetězec nemá správný formát.
v System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
v System.Double.Parse(String s)
v KalkulackaTypyVyjimek.Program.Main(String[] args) v C:\Users\...\Programy\Kalkulacka_nove\KalkulackaTypyVyjimek\KalkulackaTypyVyjimek\KalkulackaTypyVyjimek\Program.cs:řádek 29
Výstup programu 2 - Kód KalkulackaTypyVyjimek_Main: Ukázka
výsledku při chybné konverzi včetně popisu chyby
Dokážete si tipnout, jaký bude výpis v konzoli, pokud bychom zadali operaci dělení a jako jmenovatele jsme uvedli 0? Ve zdrojovém kódu vidíte, že je uvedena výjimka pro dělení nulou DivideByZeroException. Pokud se ale pozorně kouknete do výpisu v konzoli, žádný výpis chyby dělení nulou nenaleznete - viz Výstup programu 3. Jak je to možné? Datové typy proměnných jsou double. Pokud tedy přepíšeme operandy výrazu na reálná čísla, kterým skutečně jsou, tzn. 5.0 / 0.0, výsledek bude nekonečno (anglicky „Infinity“). Ve výpisu v konzoli vidíme nekonečno jako znak otazníku?. O této záludnosti jsme se již bavili v kapitole Operátory. Výsledek je zobrazen na následujícím výpisu.
Zadejte kód operace (1 ... sčítání, 2 ... odečítání, 3 ... násobení, 4 ... dělení): 4
Zadejte A: 5
Zadejte B: 0
Výsledek C: ?
Výstup programu 3 - Kód KalkulackaTypyVyjimek_Main: Ukázka výsledku nekonečno
Jaká by tedy byla hodnota výrazu 0.0 / 0.0? Obvykle, pokud vydělíte nulu nějakou hodnotou, výsledek je nula. Pokud ale vydělíte číslo nulou, výsledkem je nekonečno. Výraz 0.0 / 0.0 vede k paradoxu; hodnota musí být současně nula a nekonečno. C # má pro tuto situaci další zvláštní hodnotu zvanou NaN, což znamená, že není číslem (zkratka anglického „Not a Number“). Pokud tedy vyhodnocujete výraz 0.0 / 0.0, výsledkem je NaN. Výsledek je zobrazen na následujícím výpisu – viz Výstup programu 4.
Zadejte kód operace (1 ... sčítání, 2 ... odečítání, 3 ... násobení, 4 ... dělení): 4
Zadejte A: 0
Zadejte B: 0
Výsledek C: NaN
Výstup programu 4 - Kód KalkulackaTypyVyjimek_Main: Ukázka výsledku Not a Number
Uveďme vyhodnocení výrazu např. 5/0, pokud by oba dva operandy a, b byly deklarovány jako datový typ int. Vyhodnocení výrazu způsobí chybu a nastala by výjimka typu DivideByZeroException. V těle tohoto handleru by byla vypsána hláška, která je zobrazena ve výpisu v konzoli – viz Výstup programu 5.
Zadejte kód operace (1 ... sčítání, 2 ... odečítání, 3 ... násobení, 4 ... dělení): 4
Zadejte A: 5
Zadejte B: 0
B nesmí být 0
Výstup programu 5 - Kód KalkulackaTypyVyjimek_Main: Výjimka
typu DivideByZeroException