Perličky: objektové výjimky

14. 7. 2008
Doba čtení: 8 minut

Sdílet

V minulém díle jsme si popsali, kterak lze v Perlu použít výjimkový aparát na jednoduchých textových řetězcích. Dnes tento model rozšíříme o plnohodnotné objekty a nástroje na jejich zpracování. Dostaneme se tak na úroveň, kterou pro zpracování výjimek poskytují moderní objektově orientované jazyky.

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

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, otherwisefinally. 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::BaseError.

Modul Error poskytuje ještě menší syntaktické berličky pro vyvolání výjimek ve formě funkcí throw, withrecord. 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).

bitcoin_skoleni

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 .

Seriál: Perličky

Autor článku