Ruby po kapkách (20.) – pracujeme s výjimkami

7. srpna 2009

Máme za sebou několikeré povídání o práci se soubory a adresáři v Ruby. Tentokrát se zaměříme na nemilou věc, se kterou se setkáme relativně často právě při vstupních a výstupních operacích. Jedná se o chyby za běhu programu…

Podíváme se, jak se s nimi můžeme v Ruby elegantně vypořádat a jak korektně zpracujeme situaci, kdy se například pokoušíme otevřít neexistující soubor pro čtení nebo kdy se snažíme naopak vytvořit již existující adresář.

Tradičním problémem v softwarovém inženýrství je fakt, že k chybám dochází v programech na místech, kde nelze rozumně rozhodnout o jejich řešení. Když v aplikaci s grafickým uživatelským rozhraním zavoláme knihovní metodu pro otevření souboru s aplikačními daty a v této metodě dojde k chybě, potřebujeme, aby se informace o chybě dostala zpět do GUI. Pak bychom mohli uživateli zobrazit příslušné hlášení a nechat ho rozhodnout o dalším pokračování. Tento problém se ještě před dvaceti lety v procedurálních jazycích nepříliš obratně řešil pomocí návratových kódů a globálních struktur pro předávání informací o chybách. (Komplikovaný byl zejména přenos dostatečného množství informací potřebného k rozhodnutí o řešení chyby.) Mezitím však naštěstí došlo ke značnému pokroku a většina moderních jazyků podporuje mechanismus výjimek. Platí to pochopitelně i pro Ruby.

Výjimka je v Ruby reprezentována (jak jinak) třídou Exception. Instance této třídy zapouzdřuje informaci o chybě (respektive o nestandardní situaci, kterou nelze řešit na daném místě v kódu). Výjimka je postupně předávána výš a výš v řetězu volání metod až na místo, kde je explicitně stanoveno, že výjimku ošetřujeme. Pokud takové místo v řetězu volání není, výjimka „probublá“ až na nejvyšší úroveň, kde ukončí běh interpretu. To by se nám také stalo v případě, kdy by došlo k chybě v některém z našich příkladů na vstup a výstup uváděných v minulých kapitolách. Vezměme si jednořádkový program, který má za úkol vytvořit adresář test.

Dir.mkdir(‚test‘)

Co se stane, když adresář tohoto jména již existuje?

c:\Tmp>mkdir test
 
c:\Tmp>ruby md.rb
md.rb:1:in `mkdir‘: File exists – test (Errno::EEXIST)
from md.rb:1

Dočkali jsme se ukončení běhu programu a vypsání defaultního hlášení o výjimce. Pojďme nyní tento triviální program upravit, aby poněkud lépe simuloval realitu. Funkcionalitu vytvoření nového adresáře přesuneme do speciální metody. Hlavní část programu pak rozšíříme tak, aby při volání této metody počítala s možností vzniku výjimky.

def create_dir
Dir.mkdir(‚test‘)
end
 
begin
puts „Start“
create_dir
puts „Operace s adresářem“
rescue => e
c = e.class
while c != Object
puts c
c = c.superclass
end
puts c
puts e.message
end
puts „Konec“

Při spuštění opět dojde k výjimce (za předpokladu, že jsme mezitím nesmazali adresář test).

c:\Tmp>ruby md.rb
Start
Errno::EEXIST
SystemCallError
StandardError
Exception
Object
File exists – test
Konec

Vypíše se hlášení o startu programu. Pak se řízení předá metodě create_dir. Ta volá metodu Dir.mkdir, ve které se volá příslušná služba operačního systému a ta skončí chybou. V metodě Dir.mkdir vzniká výjimka, která je předána metodě create_dir a posléze do hlavního programu. Tam jsme však použili konstrukci s obecným tvarem (bez určení Exception za klauzulí rescue, což osvětlíme vzápětí):

begin

rescue Exception => e

end

Jedná se o blok kódu uzavřený mezi klíčová slova begin a end. První část do klíčového slova rescue je kód v němž předpokládáme vznik výjimek a chceme je ošetřovat. Jednotlivé případy, které hodláme zpracovat jsou specifikovány pomocí rescue, které se může v rámci bloku opakovat. V našem příkladu zatím očekáváme jen jednu výjimku, takže nijak nespecifikujeme a pouze si necháme zachycenou výjimku uložit pod odkazem e. V rámci zpracování pak jen vypíšeme třídu objektu v této proměnné a všechny její nadtřídy (předky) až po třídu Object (použity jsou metody class a superclass, jinak je to bežný while cyklus). Nakonec voláme metodu message objektu s výjimkou, která vrací textový popis chyby.

Ve výpisu vidíme jednu zajímavou věc. Uvedli jsme, že výjimky jsou reprezentovány třídou Exception, musíme však dodat, že také jejími mnoha podtřídami. Ty vytváří hierarchii, ve které jsou výjimky organizovány. V našem případě vidíme, že problém při vzniku adresáře je StandardError a přesněji SystemCallError a ještě přesněji Errno::EEXIST. Konkrétní podtřída ve skutečnosti sama svým typem specifikuje poměrně dost informace o výjimce a používá se mimo jiné pro určení typu chyby, který chceme ošetřovat v konkrétním rescue bloku. Upravíme příklad tak, abychom zachytili všechny chyby, ke kterým může dojít při volání služeb operačního systému, což budou typicky všechny vstupně výstupní operace. Zároveň přidáme další blok bez specifikace, který bude zachycovat všechny výjimky StandardError.

def create_dir
Dir.mkdir(‚test‘)
raise „Moje chyba“
end
 
begin
puts „Start“
create_dir
puts „Operace s adresářem“
rescue SystemCallError => e
puts „Chyba 1 – #{e.class}“
puts e.message
rescue => e
puts „Chyba 2 – #{e.class}“
puts e.message
end
puts „Konec“

Vyzkoušíme následovně:

c:\Tmp>rmdir test
 
c:\Tmp>ruby md.rb
Start
Chyba 2 – RuntimeError
Moje chyba
Konec
 
c:\Tmp>ruby md.rb
Start
Chyba 1 – Errno::EEXIST
File exists – test
Konec

Po smazání adresáře projde bez problémů jeho vytvoření, ale nově přidaný řádek s příkazem raise vyvolá výjimku typu RuntimeError se zadaným chybovým hlášením (dalšími parametry je možné určit i typ výjimky). Takto je můžeme vyvolávat výjimky z vlastního kódu, což se hodí například při psaní knihovních tříd a metod. Při druhém spuštění již adresář opět existuje a chyba tedy vznikne při jeho vytváření. Je zachycena jiným rescue blokem. Zde je třeba brát v úvahu pořadí těchto bloků – první, jehož specifikace typu výjimky odpovídá, se spustí k jejímu zpracování. Proto máme blok s obecnější specifikací jako druhý.

Ještě šířeji pojato může být blok pro zpracování výjimek organizován takto:

begin

rescue Exception => e

else

ensure

end

Již známe část rescue a víme, že se může opakovat. V části else se nachází kód, který bude vykonán v případě, že k žádné výjimce nedojde. Do částí ensure pak patří kód, který chceme vykonat, ať již k výjimce dojde či nikoliv (typicky nějaký „závěrečný úklid“.) V souladu s touto strukturou bychom finálně mohli náš příklad upravit tak, jak je uvedeno níže.

def create_dir
Dir.mkdir(‚test‘)
end
 
begin
puts „Start“
create_dir
rescue SystemCallError => e
puts „Chyba – #{e.class}“
puts e.message
else
puts „Operace s adresářem“
ensure
puts „Konec“
end

c:\Tmp>ruby md.rb
Start
Chyba – Errno::EEXIST
File exists – test
Konec
 
c:\Tmp>rmdir test
 
c:\Tmp>ruby md.rb
Start
Operace s adresářem
Konec

Za domácí úkol si rozmyslete, jak by bylo možné doplnit příklady z kapitol o vstupu a výstupu o ošetření nejběžnějších chybových stavů.

Š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 *