Problémy při používání textových výjimek
Připomeňme si ukázkový modul Rada
pro výpis řady čísel z minulého dílu:
package Rada; use Carp; use Exporter::Easy ( EXPORT => [ qw/&rada/ ] ); sub rada { my $max = shift // 0; croak "Parametr musi byt kladny" if $max <= 0; local ($\, $,) = (qq/\n/, q/ /); print (1 .. $max); }; 1;
Dejme tomu, že z nějakého důvodu budeme chtít omezit nejvyšší hodnotu $max
na pevně daný limit.
my $limit = 10; sub rada { my $max = shift // 0; croak "Parametr musi byt <= $limit" if $max > $limit; … };
Pokud bychom chtěli ve volajícím modulu tyto výjimky chytat, můžeme to udělat pomocí matchování proměnné $@
na příslušný řetězec (nebo jeho část), případně z něj i vytáhnout proměnnou $limit
:
eval { rada(15); }; if ($@ ~~ /^Parametr musi byt <= (\d+)/) { rada($1); } elsif ($@) { die $@; }
Tento způsob rozeznávání výjimek a zejména extrakce případných atributů může být velmi nepohodlný. Navíc nebezpečný, neboť stačí, aby byl modul přeložen do jiného jazyka, nebo z nějakého důvodu změněn text zprávy, a výjimka již nebude rozeznána. Samozřejmě, poslední blok elsif
by se měl o tyto případy alespoň trochu postarat, v praxi je ale celkem jednoduché na takovou věc zapomenout.
Výjimky pomocí objektů
První ze zmíněných problémů lze vyřešit elegantně tak, že namísto textové zprávy použijeme jinou datovou strukturu. Funkci die
lze předat jako parametr libovolný skalár. (Například i regexp vytvořený pomocí qr//
, ale to je zbytečně kruté.) Použijeme-li referenci na objekt, můžeme jménem třídy vyjádřit podstatu chyby a obsahem předat další parametry.
sub rada { my $max = shift // 0; die bless {}, 'Rada::ZapornyParametr' if $max <= 0; die bless { limit => $limit }, 'Rada::VelkyParametr' if $max > $limit; … }
Ve volajícím modulu pak použijeme funkci isa
pro zjištění třídy.
eval { rada(15); }; if (ref $@ and $@->isa('Rada::VelkyParametr')) { rada($@->{limit}); } elsif ($@) { die $@; }
Při použití výjimek coby objektů lze také navíc využít dědičnost. Třídy Rada::VelkyParametr
a Rada::ZapornyParametr
mohou být odvozeny od třídy
Rada::SpatnyParametr
. Pak je možné pomocí isa
kontrolovat pouze rodičovskou třídu, pakliže bychom se nechtěli zabývat
podrobnější příčinou chyby. Z druhé strany, pokud bychom rozšířili
funkci Rada
o další kontrolu parametru a tudíž
další výjimku odvozenou od Rada::SpatnyParametr
, bude náš
volající kód fungovat beze změny a správně detekovat chybu
v parametru (ačkoli nebude schopen ji analyzovat podrobněji).
Ilustrované třídy jsou implementovány pomocí klasických blessovaných hashů, nicméně je možné je implementovat i pomocí inside-out tříd, či jakéhokoliv generátoru tříd. Těchto generátorů existuje i několik s přímou specializací na třídy výjimek.
Modul Exception::Class
Použití modulu Exception::Class
osvětlí následující příklad:
package RadaException; my $limit = 10; use Exporter::Easy ( EXPORT => [ qw/&rada/ ] ); use Exception::Class ( 'SpatnyParametr', 'ZapornyParametr' => { isa => 'SpatnyParametr', }, 'VelkyParametr' => { isa => 'SpatnyParametr', fields => [ qw/limit/ ] }, ); sub rada { my $max = shift // 0; ZapornyParametr->throw('Parametr musi byt kladny') if $max gt;= 0; VelkyParametr->throw( message => 'Parametr je prilis velky', limit => $limit) if $max > $limit; local ($\, $,) = (qq/\n/, q/ /); print (1 .. $max); };
V první části pomocí parametru klauzule use
definujeme hierarchii tříd výjimek. Každá položka v seznamu vytváří zvláštní třídu, která je odvozená od základní třídy Exception::Class::Base
. Pomocí parametru isa
lze specifikovat dědičné vztahy mezi jednotlivými třídami. Parametr fields
definuje atributy příslušné třídy. Dále bychom mohli pomocí řetězcového parametru description
dodefinovat jakýsi popis výjimky, který ale není příliš často užitečný.
Ve funkci rada
pak místo die
používáme metodu throw
příslušné výjimkové třídy. Tuto metodu lze případně aliasovat jako funkci našeho modulu, např. vyjimka_zaporny_parametr
s přidaným bonusem kontroly na překlepy při překladu, což, jelikož se jedná o výjimky, se může celkem hodit – viz dokumentace. Pomocí parametrů metody
throw
můžeme naplnit naše definovaná pole objektu (např.
limit
). Další parametr, message
, specifikuje
zprávu, která bude vidět při převodu výjimky na řetězec (tj. to, co
bychom dali jako parametr řetězcového die
). Konečně lze
specifikovat ještě několik parametrů ohledně výpisu zásobníku volání,
podobně jako to dělá funkce confess
z modulu
carp
– opět viz příslušná dokumentace.
Z pohledu volajícího modulu lze výjimky odchytat několika způsoby. V duchu předchozího příkladu lze použít konstrukci s $@
:
eval { rada(15); } if (ref $@ and $@->isa('VelkyParametr')) { rada $@->limit; }
Případně můžeme z výjimky vytahat další užitečné informace:
use RadaException; use POSIX qw/ctime/; eval { rada(15); }; if (ref $@ and $@->isa('VelkyParametr')) { print 'Chycena vyjimka: ', ref $@, "\nV programu s PID: ", $@->pid, "\nV case: ", ctime($@->time), "Maximalni velikost parametru: ", $@->limit, "\nUplne trasovani:\n", $@->trace; }
Což nám dá celkem obšírný výstup:
Chycena vyjimka: VelkyParametr V programu s PID: 8987 V case: Wed Jul 9 21:27:32 2008 Maximalni velikost parametru: 10 Uplne trasovani: Trace begun at RadaException.pm line 26 RadaException::rada(15) called at vypisradu1.pl line 9 eval {...} at vypisradu1.pl line 8
(Modifikovanou a univerzálnější verzi takovéhoto výpisu si pak například můžeme nainstalovat do „defaultního“ chytače výjimek $SIG{__DIE__}
.)
Další možností odchytu výjimky je použití metody caught
, což je v principu zkratka za kombo ref and isa
.
if (my $e = Exception::Class->caught('VelkyParametr')) { … # Případně if (my $e = VelkyParametr->caught()) {
(Samozřejmě i zde bychom mohli využít dědičnost a chytat nadřazenou třídu SpatnyParametr
.)
Zkus to a chytej
Použití modulu Exception::Class::TryCatch
řeší dva problémy zároveň. Pokud máme klasický blok eval
s následnou kontrolou na výjimky, měli bychom jako pravidlo pravé ruky použít blok, který propaguje neošetřené výjimky (v tomto případě pomocí metody rethrow
), jinak bude výjimka v tichosti ignorována. Funkce catch
tento problém řeší a neošetřené výjimky automaticky propaguje dále. Seznam výjimek, které chceme ošetřit, je předán v anonymním poli a funkce se volá v bloku if
podobně jako předtím:
if (my $e = catch ['VelkyParametr', 'ZapornyParametr']) { print 'Chycena vyjimka: ', ref $e; }
Další výhodou funkce catch
je automatický převod textových výjimek (vzešlých z klasického die
) na objekty Exception::Class::Base
, tj. při ošetřování můžeme vždy počítat s výjimkou coby objektem. Nevýhoda je, že nelze funkce catch
kaskádovat za sebe a rozpoznání jednotlivých výjimek musíme udělat separátně „postaru“:
if (my $e = catch ['VelkyParametr', 'ZapornyParametr']) { print 'Chycena vyjimka: ', ref $e; print 'Maximalni hodnota: ', $e->limit if ($e->isa('VelkyParametr')); }
Druhá funkce tohoto modulu, try
, nám dává k dipozici zásobník na výjimky, a tak řeší problém vnořených nebo odložených výjimek. Namísto eval
píšeme try
eval
a ke každému try
musí existovat i příslušný catch
.
try eval { zde je něco, co používá opět try/catch } do { zde může být uklízecí kód (podobně jako bychom psali v jiných jazycích „finally“) opět s try/catch } catch { zde chytáme výjimky z prvního „try eval“ }
Modul Error
Poněkud syntakticky rozvinutější aparát pro práci s objektovými výjimkami poskytuje modul Error
, a to ve formě posloupnosti bloků try
, catch
, except
, otherwise
a finally
. Průběh bloky při různých situacích ilustruje program:
use RadaException; use Error qw/:try/; push @Exception::Class::Base::ISA, 'Error' unless Exception::Class::Base->isa('Error'); for my $p (5, 15, -5) { print "p = $p {\n\t"; try { rada($p); } catch VelkyParametr with { my $e = shift; print 'limit: ', $e->limit, qq/\n/; } otherwise { my $e = shift; print 'Chycena vyjimka: ', ref $e, qq/\n/; } finally { print "}\n"; }; } print "Konec programu\n";
Výstupem programu je:
p = 5 { 1 2 3 4 5 } p = 15 { limit: 10 } p = -5 { Chycena vyjimka: ZapornyParametr }
V prvním případě je volána funkce rada
s korektním parametrem, tedy k žádné výjimce ani jejímu zpracování nedojde. Při použití parametru s hodnotou 15 je vyvolána výjimka VelkyParametr
a zpracována příslušným blokem catch … with
. Pro parametr –5 výjimka není ošetřena zvlášt a ke slovu se tedy dostává blok otherwise
. Ve všech případech, ať s výjimkou, nebo bez, je provedena akce v bloku finally
.
Pokud blok otherwise
vynecháme, je v případě neošetřené výjimky také nejprve vykonán blok finally
(pokud existuje) a pak je taková výjimka propagována dále v kódu:
p = -5 { } Parametr musi byt kladny
Namísto ošetření výjimek blokem catch
na místě je také možno použít hash coby seznam výjimek a ošetřujících podprogramů. Tento hash se zadává pomocí bloku except
.
sub velky { my $e = shift; print 'limit: ', $e->limit, qq/\n/; } for my $p (5, 15, -5) { print "p = $p {\n\t"; try { rada($p); } except { return { VelkyParametr => \&velky } } … }
Pozorný čtenář si možná všiml hacku s push
na začátku kódu. Modul Error
poskytuje primárně vlastní třídy výjimek (kde je předdefinovaná pouze Error::Simple
, což je jakýsi ekvivalent řetězcového die
). Chceme-li používat výjimky modulu Exception::Class
, je potřeba pomocí výše zmíněného push
zavést dědičný vztah mezi třídami Exception::Class::Base
a Error
.
Modul Error
poskytuje ještě menší syntaktické berličky pro vyvolání výjimek ve formě funkcí throw
, with
a record
. Pro konkrétní formu použití těchto funkcí opět viz dokumentace modulu.
K modulu Error
pak ještě existuje modul Error::TryCatch
s téměř identickými funkcemi, nicméně namísto funkčních prototypů tento modul používá filtr zdrojového kódu. Výhodou tohoto přístupu je urychlení vašeho kódu, nevýhodou je poněkud zmatek v chybových hlášeních v případě špatné syntaxe a také možnost přepisu bloků try … catch
i v místech, kde by k tomu dojít nemělo (například v here-docech).
Závěr
Dnes jsme ukázali jak principy, které stojí za používáním výjimkových tříd, tak možnosti, kterak tyto třídy jednoduše generovat a možnosti jejich zpracování. Použití výjimek coby objektů a důslednou práci s nimi doporučují čtyři z pěti psychoterapeutů, zejména tvoříte-li cokoliv delšího než program na jednu stránku.
Příklady ke stažení: ModulyRada
s našimi vlastními výjimkami,RadaException
s výjimkami pomocí Exception::Class
a pak chytání výjimek pomocí $@, caught, catch, modulu Error
.