1. díl - Výjimky

C# .NET Práce se soubory Výjimky

V této sekci se budeme věnovat práci se soubory. Než však můžeme začít zapisovat a číst, měli bychom vyřešit, jak ošetřit chybové stavy programu, kterých při práci se soubory bude nastávat mnoho.

V našem programu může často dojít k chybě. Tím nemyslím chybě z důvodu, že byl program funkčně špatně napsaný, takových chyb se jsme schopni dobře vyvarovat. Obecně se jedná zejména o chyby, které zapříčinily tzv. vstupně/výstupní operace. V anglické literatuře se hovoří o input/output nebo zkráceně o IO. Jedná se např. o vstup uživatele z konzole, ze souboru, výstup do souboru, na tiskárnu a podobně. V zásadě platí, že zde figuruje uživatel, který nám může zadat nesmyslný vstup, neexistující nebo nevalidní soubor, odpojit tiskárnu a podobně. My však nenecháme program spadnout s chybou, naopak budeme zranitelná místa v programu ošetřovat a na danou skutečnost uživatele upozorníme.

Aktivní ošetření chyb

První možnost ošetření chyb nazýváme jako aktivní. V programu nalezneme všechna zranitelná místa a ošetříme je podmínkami. Jako učebnicový příklad se zpravidla používá dělení nulou. Představme si program, který používá třídu Matematika, která má metodu Podil. Třída by mohla vypadat např. takto:

class Matematika
{

        public static int Podil(int a, int b)
        {
                return a / b;
        }

        ...
}

Když nyní třídu použijeme takovýmto způsobem:

Console.WriteLine("Zadejte dělitele a dělence k výpočtu podílu:");
int a  = int.Parse(Console.ReadLine());
int b  = int.Parse(Console.ReadLine());
Console.WriteLine(Matematika.Podil(a, b));

Pokud nyní programu uživatel zadá čísla 12 a 0, program spadne s chybou, protože nulou nelze dělit. Aktivně chybu ošetříme jednoduchou podmínkou v programu:

Console.WriteLine("Zadejte dělitele a dělence k výpočtu podílu:");
int a  = int.Parse(Console.ReadLine());
int b  = int.Parse(Console.ReadLine());
if (b != 0)
        Console.WriteLine(Matematika.Podil(a, b));
else
        Console.WriteLine("Nulou nelze dělit.");

Nyní si musíme při každém použití metody tedy hlídat, jestli do druhého parametru nevkládáme nulu. Představte si, že by metoda brala parametrů 10 a používali jsme ji v programu několikrát. Určitě je velmi složité takto ošetřovat všechny vstupy ve všech případech.

Řešením by mohlo být vložit kontrolu přímo do metody. Máme tu však nový problém: Jakou hodnotu vrátíme, když bude 2. parametr nulový? Potřebujeme hodnotu, ze které poznáme, že výpočet neproběhl korektně. To je však problém, když zvolíme např. nulu, nepoznáme, zda 0/12 je chybný výpočet či nikoli. Nevíme, zda 0 značí výsledek nebo chybu. Ani zápornými čísly si nepomůžeme. V .NETu je podobná metoda TryParse, která vrací bool a hodnota se předává přes modifikovaný parametr. To je však nepřehledné. Parsování hodnot je 2. klasický příklad zranitelného vstupu od uživatele. Další jsou souborové operace, kde soubor nemusí existovat, nemusíme na něj mít práva, může s ním být zrovna pracováno a podobně.

Pasivní ošetření chyb

Když je operace složitější a bylo by příliš náročné ošetřovat všechny možné chybové stavy, nastupují výjimky, tzv. pasivní ošetření chyb. Nás totiž vůbec nemusí zajímat vnitřní logika v metodě, kterou voláme. Pokusíme se nebezpečnou část kódu spustit v "chráněném režimu". Tento režim je nepatrně pomalejší a liší se tím, že pokud dojde k chybě, máme možnost ji odchytit a zabránit pádu programu. O chybě zde hovoříme jako o výjimce. Využíváme k tomu tzv. try-catch bloky:

try
{
}
catch
{
}

Do bloku try umístíme nebezpečnou část kódu. Pokud nastane v bloku try chyba, jeho vykonávání se přeruší a program přejde do bloku catch. Pokud vše proběhne v pořádku, try se vykoná celý a catch se přeskočí. Vyzkoušejme si situaci na našem předchozím příkladu:

try
{
        Console.WriteLine(Matematika.Podil(a, b));
}
catch
{
        Console.WriteLine("Při dělení nastala chyba.");
}

Kód je jednodušší v tom, že nemusíme ošetřovat všechna zranitelná místa a přemýšlet, co vše by se mohlo pokazit. Nebezpečný kód pouze obalíme blokem try a všechny chyby se zachytí v catch. Samozřejmě do try-catch bloku umístíme jen to nezbytně nutné, ne celý program :)

Nyní tedy již víme, jak ošetřit situace, kdy uživatel zadává nějaký vstup, který by mohl vyvolat chybu. Nemusí se jednat jen o souborové operace, výjimky mají velmi širokou oblast použití. Ostatně např. metoda int.TryParse, kterou dobře známe a kterou jsme k ošetření vstupů používali, v sobě interně blok try-catch obsahuje. Dokážeme náš program napsat tak, aby se nedal jednoduše uživatelem shodit.

Použití výjimek při práci se soubory

Jak již bylo řečeno, souborové operace mohou vyvolat mnoho výjimek, proto se soubory vždy pracujeme v try-catch bloku. Existuje také několik dalších konstrukcí, které při výjimkách můžeme využívat.

Finally

Do try-catch bloku můžeme přidat ještě 3. blok a to finally. Ten se spustí vždy ať k výjimce došlo či nikoli. Představte si následující metodu pro uložení nastavení. Metody pro obsluhu souboru budou smyšlené:

public void UlozNastaveni()
{
        try
        {
                OtevriSoubor("soubor.dat");
                ZapisDoSouboru(nastaveni);
        }
        catch
        {
                Console.WriteLine("Chyba při zápisu do souboru.");
        }
        if (SouborJeOtevreny())
                ZavriSoubor();
}

Metoda se soubor pokusí otevřít a zapsat do něj objekt nastavení. Při chybě vypíše hlášku do konzole. Otevřený soubor musíme opět uzavřít. Vypisovat chyby přímo v metodě je však ošklivé, to ostatně již víme, metody a objekty obecně by měly provádět jen logiku a komunikaci s uživatelem obstarává ten, kdo je volá. Dejme tedy metodě návratovou hodnotu bool a vracejme true/false podle toho, zda se operace povedla či nikoli:

public bool UlozNastaveni()
{
        try
        {
                OtevriSoubor();
                ZapisDoSouboru();
                return true;
        }
        catch
        {
                return false;
        }
        if (SouborJeOtevreny())
                ZavriSoubor();
}

Na první pohled to vypadá, že se soubor vždy uzavře. Celý kód je však v nějaké metodě, ve které voláme return. Jak víme, return ukončí metodu a nic za ním se již neprovede.

Soubor by zde vždy zůstal otevřený a uzavření by se již neprovedlo. Jako následek by to mohlo mít, že by byl poté soubor nepřístupný. Pokud vložíme zavření souboru do bloku finally, vykoná se vždy. C# si pamatuje, že blok try-catch obsahoval finally a po opuštění sekce catch zavolá finally:

public bool UlozNastaveni()
{
        try
        {
                OtevriSoubor();
                ZapisDoSouboru();
                return true;
        }
        catch
        {
                return false;
        }
        finally
        {
                if (SouborJeOtevreny())
                        ZavriSoubor();
        }
}

Finally se tedy používá u výjimek na úklidové práce, dochází zde k zavírání souborů, uvolňování paměti a podobně.

Celou situaci jsem nyní značně zjednodušil. Pro každý typ souborů poskytuje C# třídu zapisovače a čteče (writer a reader). Metoda pro uložení např. nastavení by v C# reálně vypadala asi takto:

public bool UlozNastaveni()
{
        ZapisovacSouboru z = null;
        try
        {
                z = new ZapisovacSouboru("soubor.dat");
                z.Zapis(objekt);
                return true;
        }
        catch
        {
                return false;
        }
        finally
        {
                if (z != null)
                        z.Zavri();
        }
}

Takto se opravdu reálně se soubory pracuje, pouze třídu jsem si vymyslel. Do instance zapisovače umístíme nejprve null. Poté, již v bloku try, zkusíme vytvořit zapisovač na souboru soubor.dat a zapsat nějaký objekt. Pokud se vše povede, vrátíme true (samozřejmě se poté ještě zavolá blok finally). Operace může selhat ze dvou důvodů. Buď se do souboru nepovede zapsat nebo se nám soubor pro zápis ani nepovede otevřít. Výjimku v každém případě zachytíme a vrátíme false, z čeho se poté pozná, že se metodě uložení nepodařilo. Blok finally zavře soubor, který zapisovač otevřel. Jelikož se ale otevření nemuselo povést, musíme se nejprve podívat, zda se zapisovač vůbec vytvořil, abychom měli co zavírat. Metodu bychom volali např. takto:

if (!UlozNastaveni())
        Console.WriteLine("Nepodařilo se uložit nastavení.");

Blok catch můžeme vynechat a nechat metodu, aby výjimku klidně vyvolala. Budeme počítat s tím, že se s výjimkou vypořádá ten, kdo metodu zavolal, nikoli metoda sama. Je to tak lepší, ušetříme návratovou hodnotu metody (kterou lze poté použít pro něco jiného) a kód se nám zjednoduší:

public void UlozNastaveni()
{
        ZapisovacSouboru z = null;
        try
        {
                z = new ZapisovacSouboru("soubor.dat");
                z.Zapis(objekt);
        }
        finally
        {
                if (z != null)
                        z.Zavri();
        }
}

Metodu bychom nyní volali takto:

try
{
        UlozNastaveni();
}
catch
{
        Console.WriteLine("Nepodařilo se uložit nastavení.");
}

Nyní si ukážeme, jak celou situaci ještě více zjednodušit. Použijeme konstrukci using.

Using

C# umožňuje značně zjednodušit práci s instancemi tříd ke čtení a zápisu do souborů. Výše uvedený blok můžeme zapsat pomocí notace using, která nahrazuje bloky try a finally. Obrovskou výhodou je, že blok finally C# vygeneruje sám a sám zajistí, aby daná instance readeru nebo writeru soubor uzavřela. Metoda UlozNastaveni() by tedy vypadala s pomocí using takto:

public void UlozNastaveni()
{
        using (ZapisovacSouboru z = new ZapisovacSouboru("soubor.dat"))
        {
                z.Zapis(objekt);
        }
}

Vidíme, že se kód extrémně zjednodušil, i když dělá v podstatě to samé. Při volání metody opět použijeme try-catch blok. Nezapomeňte, že using nahrazuje pouze try-finally, nikoli catch!. Metodu, ve které se použivá using, musíme stejně volat v try-catch bloku.

Nyní jsme dospěli přesně tam, kam jsem chtěl. K veškerým manipulacím se soubory totiž budeme v následujících tutoriálech používat konstrukci using. Kód bude jednodušší a nikdy se nám nestane, že bychom soubor zapomněli zavřít.

K výjimkám se ještě jednou vrátíme, ukážeme si, jak odchytávat jen některé typy výjimek, které hotové třídy výjimek můžeme v našich programech používat a také, jak vytvořit výjimku vlastní. Teď jsem však chtěl vysvětlit jen potřebné minimum pro práci se soubory a ne vám zbytečně plést hlavu složitými konstrukcemi :)

Příště se podíváme, jak to funguje s právy k zápisu do souborů v systému Windows a vyzkoušíme si několik prvních souborových operací.


 

  Aktivity (1)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Celkem (7 hlasů) :
4.857144.857144.857144.857144.85714


 


Miniatura
Všechny články v sekci
Práce se soubory v C#
Miniatura
Následující článek
Úvod do práce se soubory

 

 

Komentáře
Zobrazit starší komentáře (11)

Avatar
Kit
Redaktor
Avatar
Kit:

V tom případě to má Java úplně stejně špatně. Logická a fyzická likvidace objektů musí být odděleně.

Objekty v PHP jsou často kritizovány, ale tohle v něm funguje skvěle. I v Pythonu je možné si vytvořit vlastní destruktor. Tím vlastně odpadá potřeba bloků finally, protože jsou tím zbytečné. Stačí jeden destruktor pro celý objekt.

Je na tom vidět, kam až vede honba za výkonem aplikace.

Odpovědět 16.6.2012 19:43
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
relycanx
Člen
Avatar
relycanx:

Chtěl bych se zeptat konkrétně na tento příklad:

try
{
        Console.WriteLine(Matematika.Podil(a, b));
}
catch
{
        Console.WriteLine("Při dělení nastala chyba.");
}

Asi jen špatně koukám, ale nevidím souvislost s tou nulovou podmínkou. Jak to v tomto případě může vyhodit chybu, když tam není podmínka? Jako jinak využití příkazů try a catch myslím chápu. Když se např. soubor v počítači nenajde, tak to vyplivne catch a nemusí tam být vyloženě zadaná podmínka, ale tady prosím o vysvětlení, jestli můžu :)

 
Odpovědět 5.4.2013 11:19
Avatar
Petr Nymsa
Redaktor
Avatar
Odpovídá na relycanx
Petr Nymsa:

Nelez dělit nulou, proto vyskočí vyjímka :)

Odpovědět 5.4.2013 11:33
Pokrok nezastavíš, neusni a jdi s ním vpřed
Avatar
relycanx
Člen
Avatar
Odpovídá na Petr Nymsa
relycanx:

Jo takhle, takže to bere Virtual Studio jako samozřejmost, chápu, dík :) Já právě nevěděl, jestli má tuto podmínku v paměti.

 
Odpovědět 5.4.2013 11:49
Avatar
Kit
Redaktor
Avatar
Odpovídá na relycanx
Kit:

Obvykle se nebalí každý riskantní příkaz do jednoho try..catch, ale téměř vždy nějaký blok příkazů. Často se ani neošetřuje uvnitř metody, ve které může k výjimce dojít.

Odpovědět 5.4.2013 13:30
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
relycanx
Člen
Avatar
Odpovídá na Kit
relycanx:

No pochopil jsem to asi tak, že try je jakási "databáze" podmínek a když z nich jedna nesouhlasí, tak to vyhodí catch a proto je celý ten blok try-catch pomalejší, protože se musí nejdříve vyhodnotit celý ten seznam, je to tak, nebo jsem vedle? :D

 
Odpovědět 5.4.2013 13:44
Avatar
Kit
Redaktor
Avatar
Odpovídá na relycanx
Kit:

Jsi vedle. try..catch je rychlejší.

Výjimka není databáze, ale objekt.

Odpovědět 5.4.2013 13:50
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
relycanx
Člen
Avatar
Odpovídá na Kit
relycanx:

ajaj :D :) a dají se tam nějakým způsobem vkládat další, individuální podmínky, aby se dala vyjímka používat celkově jako zábrana proti chybám namísto if?

 
Odpovědět 5.4.2013 14:00
Avatar
Kit
Redaktor
Avatar
Odpovídá na relycanx
Kit:

Jistě. Na to je klíčové slovo throw. Jenom si musíš dát pozor, abys pomocí výjimek nedělal flow-control. Program by se ti zpomalil a je to špatně i z hlediska návrhu. Výjimky musí zůstat výjimkami, musí k nim docházet jen při výjimečných situacích a při chybách.

Výjimkou je např. i chybně vyplněné formulářové pole uživatelem nebo špatný parametr programu, ale určitě ne podmínka pro ukončení cyklu.

Odpovědět 5.4.2013 14:05
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na relycanx
David Čápka:

Tento článek zde má pokračování, vše se v něm dozvíš :)

Odpovědět  +1 5.4.2013 14:08
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 21. Zobrazit vše