1. diel - Výnimky
V tomto C# .NET kurze sa budeme venovať práci so súbormi. Než však môžeme začať zapisovať a čítať, mali by sme vyriešiť, ako ošetriť chybové stavy programu, ktorých pri práci so súbormi bude nastávať mnoho.
V našom programe môže často dôjsť k chybe. Tým nemyslím chybe z dôvodu, že bol program funkčne zle napísaný, takýchto chýb sa sme schopní dobre vyvarovať. Všeobecne ide najmä o chyby, ktoré zapríčinili takzvané vstupno/výstupné operácie. V anglickej literatúre sa hovorí o input/output alebo skrátene o IO. Ide napr. o vstup užívateľa z konzoly, zo súboru, výstup do súboru, na tlačiareň a podobne. V zásade platí, že tu figuruje užívateľ, ktorý nám môže zadať nezmyselný vstup, neexistujúci alebo nevalidný súbor, odpojiť tlačiareň a podobne. My však nenecháme program spadnúť s chybou, naopak budeme zraniteľné miesta v programe ošetrovať a na danú skutočnosť používateľa upozorníme.
Aktívne ošetrenie chýb
Prvú možnosť ošetrenia chýb nazývame ako aktívnu. V programe zmapujeme
všetky zraniteľné miesta a ošetríme ich podmienkami. Ako učebnicový
príklad sa spravidla používa delenie nulou. Predstavme si program, ktorý
používa triedu Mathematics
, ktorá má metódu
Divide()
. Trieda by mohla vyzerať napríklad takto:
class Mathematics { public static int Divide(int a, int b) { return a / b; } // ... }
Teraz triedu použijeme takýmto spôsobom:
Console.WriteLine("Enter the dividend and the divisor to calculate the result:"); int a = int.Parse(Console.ReadLine()); int b = int.Parse(Console.ReadLine()); Console.WriteLine(Mathematics.Divide(a, b));
Ak teraz programu užívateľ zadá čísla 12
a 0
,
program spadne s chybou, pretože nulou nemožno deliť. Aktívne chybu
ošetríme jednoduchou podmienkou v programe:
Console.WriteLine("Enter the dividend and the divisor to calculate the result:"); int a = int.Parse(Console.ReadLine()); int b = int.Parse(Console.ReadLine()); if (b != 0) Console.WriteLine(Mathematics.Divide(a, b)); else Console.WriteLine("You cannot divide by zero.");
Teraz si musíme pri každom použití metódy teda strážiť, či do druhého parametra nevkladáme nulu. Predstavte si, že by metóda brala parametrov 10 a používali sme ju v programe niekoľkokrát. Určite by bolo veľmi zložité ošetrovať všetky použitia tejto metódy.
Riešením by mohlo byť vložiť kontrolu priamo do metódy. Máme tu však
nový problém: Akú hodnotu vrátime, keď bude druhý parameter nulový?
Potrebujeme hodnotu, z ktorej spoznáme, že výpočet neprebehol korektne. To
je však problém, keď zvolíme napríklad nulu, nepoznáme, či napríklad
0/12
je chybný výpočet alebo nie. Nevieme, či 0
značí výsledok alebo chybu.
Ani zápornými číslami si nepomôžeme. V .NETe je podobná metóda
TryParse()
, ktorá vracia bool
a hodnota sa odovzdáva
cez modifikovaný parameter. To je však neprehľadné. Parsovanie hodnôt je
druhý klasický príklad zraniteľného vstupu od užívateľa. Ďalšie sú
súborové operácie, kde súbor nemusí existovať, nemusíme naň mať práva,
môže s ním byť práve pracované a podobne.
Pasívne ošetrenie chýb
Najmä, keď je operácia zložitejšia a bolo by príliš náročné
ošetrovať všetky možné chybové stavy, nastupujú
výnimky, takzvané pasívne ošetrenie chýb. Nás totiž
vôbec nemusí zaujímať vnútorná logika v metóde, ktorú voláme. Pokúsime
sa nebezpečnú časť kódu spustiť v "chránenom režime".
Tento režim je nepatrne pomalší a líši sa tým, že pokiaľ dôjde k chybe,
máme možnosť ju odchytiť a zabrániť pádu programu. O chybe tu hovoríme
ako o výnimke. Využívame na to takzvané try
-
catch
bloky:
try { } catch { }
Do bloku try
umiestnime nebezpečnú časť
kódu. Pokiaľ nastane v bloku try
chyba, jeho
vykonávanie sa preruší a program prejde do bloku catch
. Pokiaľ
všetko prebehne v poriadku, try
sa vykoná celý a
catch
sa preskočí. Vyskúšajme si situáciu na našom
predchádzajúcom príklade:
try { Console.WriteLine(Mathematics.Divide(a, b)); } catch { Console.WriteLine("An error occurred while dividing."); }
Kód je jednoduchší v tom, že nemusíme ošetrovať všetky zraniteľné
miesta a premýšľať, čo všetko by sa mohlo pokaziť. Nebezpečný kód iba
obalíme blokom try
a všetky chyby sa zachytia v
catch
. Samozrejme do try
- catch
bloku
umiestnime len to nevyhnutné, nie celý program
Teraz teda už vieme, ako ošetriť situácie, keď používateľ zadáva
nejaký vstup, ktorý by mohol vyvolať chybu. Nemusí ísť len o súborové
operácie, výnimky majú veľmi širokú oblasť použitia. Ostatne napríklad
metóda int.TryParse()
, ktorú dobre poznáme a ktorú sme na
ošetrenie vstupov používali, by išla napísať pomocou try
-
catch
bloku. Dokážeme náš program napísať tak, aby sa nedal
jednoducho používateľom zhodiť.
Použitie výnimiek pri práci so súbormi
Ako už bolo povedané, súborové operácie môžu vyvolať mnoho výnimiek,
preto sa súbory vždy pracujeme v try
- catch
bloku.
Existuje aj niekoľko ďalších konštrukcií, ktoré pri výnimkách môžeme
využívať.
Blok finally
Do try
- catch
bloku môžeme pridať ešte tretí
blok, a to finally
. Ten sa spustí vždy nech k výnimke
došlo alebo nie. Predstavme si nasledujúcu metódu na uloženie
nastavení. Metódy pre obsluhu súboru budú vymyslené:
public void SaveSettings() { try { OpenFile("file.dat"); WriteToFile(settings); } catch { Console.WriteLine("An error has occurred while writting to the file."); } if (FileIsOpened()) CloseFile(); }
Metóda sa súbor pokúsi otvoriť a zapísať do neho objekt nastavenia. Pri
chybe vypíše hlášku do konzoly. Otvorený súbor musíme opäť uzavrieť.
Vypisovať chyby priamo v metóde je však škaredé, to napokon už vieme.
Metódy a objekty všeobecne by mali vykonávať len logiku a komunikáciu s
užívateľom obstaráva ten, kto ich volá. Dajme teda metóde návratovú
hodnotu bool
a vracajme true
/ false
podľa toho, či sa operácia podarila alebo nie:
public bool SaveSettings() { try { OpenFile(); WriteToFile(); return true; } catch { return false; } if (FileIsOpened()) CloseFile(); }
Na prvý pohľad to vyzerá, že sa súbor vždy uzavrie. Celý kód je však
v nejakej metóde, v ktorej voláme return
. Ako vieme,
return
ukončí metódu a nič za ním sa už nevykoná. Súbor by
tu vždy zostal otvorený a uzavretie by sa už nevykonalo. Ako následok by to
mohlo mať, že by bol potom súbor neprístupný.
Pokiaľ vložíme zatvorenie súboru do bloku
finally
, vykoná sa vždy.
C# si pamätá, že blok try
- catch
obsahoval
finally
a zavolá finally
blok aj po opustení bloku
catch
alebo try
:
public bool SaveSettings() { try { OpenFile(); WriteToFile(); return true; } catch { return false; } finally { if (FileIsOpened()) CloseFile(); } }
Blok finally
sa teda používa pri výnimkách na
upratovacie práce, dochádza tu k zatváraniu súborov,
uvoľňovaniu pamäte a podobne.
Celú situáciu sme teraz značne zjednodušili. Pre každý typ súborov poskytuje C# triedu zapisovača a čítača (writer a reader). Metóda na uloženie napríklad nastavenia by v C# reálne vyzerala asi takto:
public bool SaveSettings() { SomeFileWriter someFileWriter = null; try { someFileWriter = new SomeFileWriter("file.dat"); someFileWriter.Write(obj); return true; } catch { return false; } finally { if (someFileWriter != null) someFileWriter.Close(); } }
Už sa blížime spôsobu, akým budeme so súbormi naozaj reálne pracovať,
iba triedu sme si vymysleli. Do inštancie zapisovača umiestnime najprv
null
. Potom, už v bloku try
, skúsime vytvoriť
zapisovač na súbore file.dat
a zapísať nejaký objekt. Ak sa
všetko podarí, vrátime true
(samozrejme sa potom ešte zavolá
blok finally
).
Operácia môže zlyhať z dvoch dôvodov. Buď sa do súboru nepodarí
zapísať alebo sa nám súbor pre zápis ani nepodarí otvoriť. Výnimku v
každom prípade zachytíme a vrátime false
, z čoho sa potom
pozná, že sa metóde uloženia nepodarilo. V bloku finally
uzavrieme súbor, ktorý zapisovač otvoril. Keďže sa ale otvorenie nemuselo
podariť, musíme sa najskôr pozrieť, či sa zapisovač vôbec vytvoril, aby
sme mali čo zatvárať. Metódu by sme volali napríklad takto:
if (!SaveSettings()) Console.WriteLine("Unable to save settings.");
Blok catch
by bolo najlepšie úplne vynechať a nechať
metódu, aby výnimku pokojne vyvolala. Budeme počítať s tým, že sa s
výnimkou vysporiada ten, kto metódu zavolal, nie metóda sama. Je to tak
lepšie, ušetríme návratovú hodnotu metódy (ktorú je potom možné
použiť pre niečo iné) a kód sa nám zjednoduší:
public void SaveSettings() { SomeFileWriter someFileWriter = null; try { someFileWriter = new SomeFileWriter("file.dat"); someFileWriter.Write(obj); } finally { if (someFileWriter != null) someFileWriter.Close(); } }
Metódu by sme teraz volali takto:
try { SaveSettings(); } catch { Console.WriteLine("Unable to save settings."); }
Teraz si ukážeme, ako celú situáciu ešte viac zjednodušiť. Použijeme
konštrukciu using
.
Konštrukcie using
C# umožňuje značne zjednodušiť prácu s inštanciami tried na čítanie
a zápis do súborov. Vyššie uvedený blok môžeme zapísať pomocou notácie
using
, ktorá nahrádza bloky try
a
finally
. Obrovskou výhodou je, že blok finally
C#
vygeneruje sám a sám zaistí, aby daná inštancia readeru alebo writera
súbor uzavrela. Metóda SaveSettings()
by teda vyzerala s pomocou
using
takto:
public void SaveSettings() { using (SomeFileWriter someFileWriter = new SomeFileWriter("file.dat")) { someFileWriter.Write(obj); } }
Vidíme, že sa kód extrémne zjednodušil, aj keď robí v podstate to
isté. Pri volaní metódy opäť použijeme try
-
catch
blok.
Nezabudnime, že notácie using
nahrádza iba
try
- finally
, nie catch
! Metódu, v
ktorej sa používa using
, musíme rovnako volať v
try
- catch
bloku.
Teraz sme dospeli presne tam, kam sme chceli. Na všetky manipulácie so
súbormi totiž budeme v nasledujúcich tutoriáloch používať konštrukciu
using
. Kód bude jednoduchší a nikdy sa nám nestane, že by sme
súbor zabudli zavrieť.
K výnimkám sa ešte raz vrátime, ukážeme si, ako odchytávať len niektoré typy výnimiek, ktoré hotové triedy výnimiek môžeme v našich programoch používať a tiež ako vytvoriť výnimku vlastnú. Teraz sme si len vysvetlili potrebné minimum pre prácu so súbormi
V nasledujúcej lekcii, Úvod do práce so súbormi, sa pozrieme, ako to funguje s právami na zápis do súborov v systéme Windows a vyskúšame si niekoľko prvých súborových operácií.