Ruby po kapkách (21.) – ošetření výjimek

14. srpna 2009

Naučili jsme se, jak Ruby pracuje s chybovými stavy či výjimkami. Zjistili jsme, že informace o nich zapouzdřuje do instancí podtříd třídy Exception a podívali jsem se na konstrukce jazyka, které umožňují výjimky v daném bloku kódu selektivně zachytit a podle potřeby zpracovat. V předchozích článcích jsme vytvořili několik jednoduchých programů, které možný výskyt chyb nijak neřešily. Pojďme je teď doplnit tak, aby dokázaly alespoň informovat uživatele a korektně skončit.

Jedním z užitečných i když jednoduchých příkladů byl program na kopírování souboru, který se objevil na začátku 18. dílu. Zdrojový kód pro připomenutí je zde.

f1 = File.new(ARGV[0])
f2 = File.new(ARGV[1], ‚w‘)
while buffer = f1.read(2048) do
f2.write(buffer)
end
f1.close
f2.close

Jaké potenciální chyby mohou v takto krátkém kódu vznikat? Největším zdrojem problémů jsou samozřejmě operace vstupu a výstupu. V prvních dvou řádcích se otevíra jeden soubor pro čtení a druhý pro zápis. Soubor pro čtení nemusí existovat a v obou případech nemusí mít náš proces dostatečná práva na čtení nebo zápis do souboru. Pokud se to stane, dojde k výjimce, která je reprezentována některou podtřídou SystemCallError. Pak následuje smyčka, která zajištuje samotné kopírování. Střídají se v ní metody pro čtení a zápis a v obou případech může opět dojít k chybě – řekněme v podobě defektu média nebo zaplnění disku.

Zbývá nám ošetřit ještě jednu situaci: uživatel může zadat nesprávný počet parametrů příkazové řádky, z nichž se načítají názvy souborů. V rámci standardizace se pokusíme ošetřit i tento stav jako výjimku, kterou ovšem sami vyvoláme. Možné řešení ukazuje výpis.

begin
raise unless ARGV.size == 2
f1 = File.new(ARGV[0])
f2 = File.new(ARGV[1], ‚w‘)
while buffer = f1.read(2048) do
f2.write(buffer)
end
rescue RuntimeError => e
# Nesprávný počet parametrů
puts „Použití: ruby #{$0} zdroj cíl“
rescue SystemCallError => e
# Chyba vstupu/výstupu
puts ‚Došlo k chybě při operaci vstupu nebo výstupu:‘
puts e.message
else
puts ‚Kopírování úspěšně dokončeno.‘
ensure
f1.close unless f1.nil?
f2.close unless f2.nil?
end

V takto jednoduchém případě je asi nejlepší uzavřít celý program do jednoho bloku pro zachycení výjimek. (Obecně může být bloků více a je možné je vnořovat.) Na začátku provedeme test na správný počet parametrů a v případě jiného čísla než 2 vyvoláme vlastní výjimku. Ta je zachycena první klauzulí rescue (je to instance RuntimeError). Druhá klauzule rescue zachycuje chyby vstupně výstupních operací. V případě, že k žádné výjimce nedojde, informujeme uživatele o úspěšném provedení programu. V každém případě (klauzule ensure) se pokusíme o uzavření souborů, ale pouze v případě, že soubor byl otevřen. Proto testujeme příslušné proměnné, zda jsou nastaveny.

c:\Tmp>ruby copy2.rb
Použití: ruby copy2.rb zdroj cíl
 
c:\Tmp>ruby copy2.rb neexistuje tam
Došlo k chybě při operaci vstupu nebo výstupu:
No such file or directory – neexistuje
 
c:\Tmp>mkdir test
 
c:\Tmp>ruby copy2.rb copy2.rb test
Došlo k chybě při operaci vstupu nebo výstupu:
Permission denied – test

Ani náš upravený program není pochopitelně „neprůstřelný“ v tom smyslu, že pořád může skončit pádem interpretu na nezachycené výjimce. Z praktických důvodů totiž zachytáváme jen „očekávané“ výjimky. Tedy ty, které lze rozumně předvídat. S velmi malou pravděpodobností však stále mohou nastat situace (například nedostatek operační paměti pro interpret), které nijak ošetřeny nemáme, protože stejně není jasné, jak bychom je měli řešit. V případě složitější aplikace je zřejmě vhodné zachytávat všechny výjimky, aby kvůli drobnosti, na kterou jsme nemysleli, nedošlo k ukončení aplikace a potenciální ztrátě dat. Otázkou však je, zda aplikace skutečně může bez problémů pokračovat.

Namísto pokračování ve filozofických úváhách si raději ukážeme další dvě možnosti, jak se zachovat při ošetření chyb. K tomu opět použijeme jednoduchý příklad programu. Je určen ke spočítání řádků v souboru se zdrojovým kódem v Ruby. Název souboru se předává tradičně jako parametr z příkazové řádky. Chceme řešit situaci, že někdy může uživatel zadat název souboru včetně koncového „.rb“ a někdy bez. Namísto běžných řídících struktur použijeme mechanismus ošetření výjimek.

name = ARGV[0] begin
f = File.open(name)
c = 0
f.each_line { c += 1 }
f.close
puts „Počet řádků v souboru #{name}: #{c}“
rescue Errno::ENOENT => e
if name !~ /\.rb/
name += ‚.rb‘
retry
else
raise
end
end

V klauzuli rescue nyní zachytáváme skutečně jen konkrétní chybu neexistujícího souboru. Povšimněte si, že název souboru jsme si hned na začátku programu uložili do proměnné name. Nyní při ošetření výjimky pomocí regulárního výrazu testujeme, zda má název koncovku. Pokud nemá, doplníme řetězec „.rb“ na konec a pomocí příkazu retry spustíme od začátku blok kódu začínající klíčovým slovem begin. Jinými slovy v rámci ošetření výjimky mírně pozměníme parametry chodu programu a zkusíme problematickou část programu znovu. V našem případě provádíme další pokus jen v případě chybějící koncovky. Obecně je možné provádět opakování libovolně mnohokrát. Program funguje přesně, jak jsme chtěli.

c:\Tmp>ruby countruby.rb copy2.rb
Počet řádků v souboru copy2.rb: 20
 
c:\Tmp>ruby countruby.rb copy2
Počet řádků v souboru copy2.rb: 20

Zbývá dořešit, co se stane v případě, kdy soubor opravdu neexistuje ani s koncovkou ani bez. V programu to řešíme velmi jednoduše – pošleme výjimku o kontext výš. V ukázce tedy až na úroveň interpretu, který se proto ukončí. Použijeme k tomu příkaz raise, který ale v kontextu bloku rescue má takovou funkci, že opětovně vyvolá výjimku, která je příslušnou klauzulí rescue aktuálně zachycena.

c:\Tmp>ruby countruby.rb copy3
countruby.rb:3:in `initialize‘: No such file or directory – copy3.rb (Errno::ENOENT)
from countruby.rb:3:in `open‘
from countruby.rb:3

Je vidět, že konstrukce používané k zajištění běhu programu při vzniku výjimečných událostí jsou vlastně alternativní řídící struktury. Ruby nabízí ještě jednu konstrukci, která je obzvláště vhodná k rychlému návratu z hluboce vnořených cyklů nebo volání metod. Jedná se o kombinaci příkazů catch a throw. V reálném kódu se však moc často nevidí, protože dobře napsané programy v Ruby obvykle nemají potřebu podobných téměř nouzových řešení. Školní příklad jsem si proto vypůjčil z knihy Programming Ruby.

def prompt_and_get(prompt)
print prompt
res = readline.chomp
throw :quit_requested if res == „!“
res
end
 
catch :quit_requested do
name = prompt_and_get(„Name: „)
age = prompt_and_get(„Age: „)
sex = prompt_and_get(„Sex: „)
# ..
# process information
end

Úkolem kódu je načítat postupně informace od uživatele s tím, že uživatel může v kterémkoliv okamžiku program ukončit zadáním znaku „!“. K načítání je nadefinována metoda prompt_and_get. Za ní následuje blok uvozený příkazem catch a identifikátorem (řetězec začínající dvojtečkou). Kdekoliv pak dojde k vykonání příkazu throw, bude interpret hledat postupně zpětně v řetězu volání, až najde blok se stejným identifikátorem. Pokračovat bude za koncem tohoto bloku.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *