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