Lekce 1 - Výjimky
V tomto C# .NET kurzu 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
zmapujeme 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; } ... }
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ě by bylo velmi složité ošetřovat všechna použití této metody.
Ř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 např.
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
Zejména, 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, by šla napsat pomocí try
-catch
bloku. 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 zavolá finally
blok i po opuštění bloku
catch
nebo try
:
public bool UlozNastaveni() { try { OtevriSoubor(); ZapisDoSouboru(); return true; } catch { return false; } finally { if (SouborJeOtevreny()) ZavriSoubor(); } }
Blok 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 zapisovacSouboru = null; try { zapisovacSouboru = new ZapisovacSouboru("soubor.dat"); zapisovacSouboru.Zapis(objekt); return true; } catch { return false; } finally { if (zapisovacSouboru != null) zapisovacSouboru.Zavri(); } }
Již se blížíme způsobu, jakým budeme se soubory opravdu reálně
pracovat, 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. V bloku finally
uzavřeme 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
by bylo nejlepší úplně 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 zapisovacSouboru = null; try { zapisovacSouboru = new ZapisovacSouboru("soubor.dat"); zapisovacSouboru.Zapis(objekt); } finally { if (zapisovacSouboru != null) zapisovacSouboru.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 zapisovacSouboru = new ZapisovacSouboru("soubor.dat")) { zapisovacSouboru.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ňme, že notace using
nahrazuje pouze
try
-finally
, nikoli catch
!. Metodu, ve
které se použivá using
, musíme stejně volat v
try
-catch
bloku. .[warning]
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
V příští lekci, Úvod do práce se soubory, 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í.