1. diel - Použitie výnimiek pri práci so súbormi v Kotlin
Vitajte v tutoriále venovanému práci so súbormi v Kotline. V dnešnom dieli si ukážeme, ako ošetriť chybové stavy programu, ktorých pri práci so súbormi bude nastávať veľa. Predstavíme si výnimky, vďaka ktorým budeme môcť začať bezpečne do súborov zapisovať alebo z nich čítať.
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 sme schopní sa vyvarovať. Všeobecne ide najmä o chyby, ktoré zapríčinili tzv. 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ívna. V programe nájdeme
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 Matematika
, ktorá má metódu
vydel()
. Trieda by mohla vyzerať napr. takto:
class Matematika { companion object { fun vydel(a: Int, b: Int): Int { return a / b } } ... }
Teraz triedu použijeme takýmto spôsobom:
println("Zadejte dělitele a dělence k výpočtu podílu:") val a: Int = readLine().toString().toInt() val b: Int = readLine().toString().toInt() println(Matematika.vydel(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:
println("Zadejte dělitele a dělence k výpočtu podílu:") val a: Int = readLine().toString().toInt() val b: Int = readLine().toString().toInt() if (b != 0) println(Matematika.vydel(a, b)) else println("Nulou nelze dělit.")
Teraz si musíme pri každom použití metódy strážiť, či do druhého parametra nevkladáme nulu. Predstavte si, že by metóda brala parametrov desať a používali sme ju v programe niekoľkokrát. Určite je veľmi zložité takto ošetrovať všetky vstupy vo všetkých prípadoch.
Riešením by mohlo byť vložiť kontrolu priamo do metódy. Narazíme však
na nový problém. Akú hodnotu vrátime, keď bude druhý parameter nulový?
Potrebujeme hodnotu, z ktorej spoznáme, že výpočet neprebehol korektne. A
akú hodnotu môžeme použiť? Keď zvolíme napr. nulu, nepoznáme, či
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. Mohli
by sme použiť nullovateľný typ, ale ide to aj jednoduchšie a
správnejšie.
Parsovanie hodnôt je druhý klasický príklad zraniteľného vstupu od použí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
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, tzv.
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 tzv. try-catch
bloky:
try { // kód, který může vyhodit výjimku } catch (e: Exception) { // kód, který se provede při zachycení výjimky }
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 { println(Matematika.vydel(a, b)) } catch (e: Exception) { println("Při dělení nastala chyba") }
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. Dokážeme náš program napísať tak, aby sa nedal jednoducho používateľom zhodiť.
V minulosti sme už bloky try
a catch
použili
niekoľkokrát, bolo to najmä pri parsovaní dátumu a času.
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, či k výnimke došlo, alebo
nie.
Predstavte si nasledujúcu metódu na uloženie nastavení. Metódy pre obsluhu súboru budú vymyslené:
fun ulozNastaveni() { try { otevriSoubor("soubor.dat") zapisDoSouboru(nastaveni) } catch (e: Exception) { println("Chyba při zápisu do souboru.") } if (souborJeOtevreny()) { zavriSoubor() } }
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 Boolean
a vracajme true/false
podľa toho, či
sa operácia podarila alebo nie:
fun ulozNastaveni(): Boolean { return try { otevriSoubor() zapisDoSouboru() true } catch (e: Exception) { false } if (souborJeOtevreny()) { zavriSoubor() } }
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. Kotlín si pamätá, že blok try-catch
obsahoval finally
a po opustení sekcie catch
zavolá
finally
:
fun ulozNastaveni(): Boolean { return try { otevriSoubor() zapisDoSouboru() true } catch (e: Exception) { false } finally { if (souborJeOtevreny()) { zavriSoubor() } } }
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.
Zapisovanie do súborov
Pre každý typ súborov poskytuje Kotlin triedu zapisovača a čítača (writer a reader). Metóda na uloženie napr. nastavenia by v Kotline reálne vyzerala asi takto:
fun ulozNastaveni(): Boolean { var z: ZapisovacSouboru? = null return try { z = ZapisovacSouboru("soubor.dat") z.zapis(objekt) true } catch (e: Exception) { false } finally { if (z != null) { z.zavri() } } }
Takto sa naozaj reálne so súbormi pracuje, iba triedu som si vymyslel. Do
inštancie zapisovača umiestnime najprv null
. Potom, už v bloku
try
, skúsime vytvoriť zapisovač na súbore
soubor.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. Blok finally
zatvorí
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. takto:
if (!ulozNastaveni()) println("Nepodařilo se uložit nastavení.")
Blok catch
môžeme 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ší:
fun ulozNastaveni() { var z: ZapisovacSouboru? = null try { z = ZapisovacSouboru("soubor.dat") z.zapis(objekt) } finally { if (z != null) z.zavri() } }
Metódu by sme teraz volali takto:
try { ulozNastaveni(); } catch (e: Exception) { println("Nepodařilo se uložit nastavení.") }
Teraz si ukážeme, ako celú situáciu ešte viac zjednodušiť. Použijeme
konštrukciu try-with-resources
.
Notácia
try-with-resources
Kotlín 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 tzv. notácie try-with-resources
, ktorá nahrádza
bloky try
a finally
. Obrovskou výhodou je,
že blok finally
Kotlin vygeneruje sám a zaistí, aby daná
inštancia readeru alebo writera súbor uzavrela.
Pre zápis try-with-resources
bloku použijeme v
Kotline kľúčové use
nasledované lambda výrazom.
Metóda ulozNastaveni()
by teda vyzerala takto:
fun ulozNastaveni() { ZapisovacSouboru("soubor.dat").use { z -> z.zapis(objekt) } }
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.
Nezabudnite, že try-with-resources
nahrádza
iba try-finally
, nie catch
! Metódu, v ktorej sa
používa try-with-resources
, musíme rovnako volať v
try-catch
bloku.
Teraz sme dospeli presne tam, kam som chcel. Na všetky manipulácie so
súbormi totiž budeme v nasledujúcich tutoriáloch používať konštrukciu
try-with-resources
. 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ú.
V budúcej lekcii, Triedy pre prácu so súbormi v Kotlin , si ukážeme jednotlivé typy súborov a povieme
si, ako fungujú prístupové práva v operačných systémoch a ako sa
vytvárajú inštancie tried Path
a File
.