Lekce 1 - Použití výjimek při práci se soubory v Kotlin
Vítejte v tutoriálu věnovanému práci se soubory v Kotlin. V dnešním dílu si ukážeme, jak ošetřit chybové stavy programu, kterých při práci se soubory bude nastávat mnoho. Představíme si výjimky, díky nimž budeme moci začít bezpečně do souborů zapisovat a nebo z nich číst.
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 jsme schopni se 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
vydel()
. Třída by mohla vypadat např. takto:
class Matematika { companion object { fun vydel(a: Int, b: Int): Int { return a / b } } ... }
Nyní třídu použijeme takovýmto způsobem:
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))
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:
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.")
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ů deset 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. Narazíme však
na nový problém. Jakou hodnotu vrátíme, když bude druhý parametr nulový?
Potřebujeme hodnotu, ze které poznáme, že výpočet neproběhl korektně. A
jakou hodnotu můžeme použít? 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. Mohli
bychom použít nullovatelný typ, ale jde to i jednodušeji a správněji.
Parsování hodnot je druhý 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 { // kód, který může vyhodit výjimku } catch (e: Exception) { // kód, který se provede při zachycení výjimky }
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 { println(Matematika.vydel(a, b)) } catch (e: Exception) { println("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í. Dokážeme náš program napsat tak, aby se nedal jednoduše uživatelem shodit.
V minulosti jsme již bloky try
a catch
použili
několikrát, bylo to zejména u parsování data a času.
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.
Blok Finally
Do try-catch
bloku můžeme přidat ještě třetí 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é:
fun ulozNastaveni() { try { otevriSoubor("soubor.dat") zapisDoSouboru(nastaveni) } catch (e: Exception) { println("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 Boolean
a vracejme true/false
podle toho, zda
se operace povedla či nikoli:
fun ulozNastaveni(): Boolean { return try { otevriSoubor() zapisDoSouboru() true } catch (e: Exception) { 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. Kotlin si pamatuje, že blok try-catch
obsahoval
finally
a po opuštění sekce catch
zavolá
finally
:
fun ulozNastaveni(): Boolean { return try { otevriSoubor() zapisDoSouboru() true } catch (e: Exception) { 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ě.
Zapisování do souborů
Pro každý typ souborů poskytuje Kotlin třídu zapisovače a čteče (writer a reader). Metoda pro uložení např. nastavení by v Kotlin reálně vypadala 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 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()) println("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ší:
fun ulozNastaveni() { var z: ZapisovacSouboru? = null try { z = ZapisovacSouboru("soubor.dat") z.zapis(objekt) } finally { if (z != null) z.zavri() } }
Metodu bychom nyní volali takto:
try { ulozNastaveni(); } catch (e: Exception) { println("Nepodařilo se uložit nastavení.") }
Nyní si ukážeme, jak celou situaci ještě více zjednodušit. Použijeme
konstrukci try-with-resources
.
Notace try-with-resources
Kotlin 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í tzv. notace
try-with-resources
, která nahrazuje bloky try
a finally
. Obrovskou výhodou je, že blok
finally
Kotlin vygeneruje sám a zajistí, aby daná instance
readeru nebo writeru soubor uzavřela.
Pro zápis try-with-resources
bloku použijeme v
Kotlin klíčové use
následované lambda výrazem.
Metoda ulozNastaveni()
by tedy vypadala takto:
fun ulozNastaveni() { ZapisovacSouboru("soubor.dat").use { z -> 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 try-with-resources
nahrazuje
pouze try-finally
, nikoli catch
! Metodu, ve které se
použivá try-with-resources
, 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
try-with-resources
. 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í.
V příští lekci, Třídy pro práci se soubory v Kotlin, si ukážeme jednotlivé typy souborů a řekneme
si, jak fungují přístupová práva v operačních systémech a jak se
vytvářejí instance tříd Path
a File
.