Cosa c’è di rotto sulle eccezioni in Perl?

Una discussione in un’altra domanda mi ha fatto riflettere: che cosa hanno i sistemi di eccezione di altri linguaggi di programmazione?

Le eccezioni built-in di Perl sono un po ‘ ad-hoc in quanto erano, come il sistema di oggetti Perl 5, sdoppiate in un secondo momento, e sovraccaricano altre parole chiave ( eval e die ) che non sono specificamente dedicate alle eccezioni.

La syntax può essere un po ‘brutta, rispetto alle lingue con syntax incorporata try / throw / catch. Di solito lo faccio in questo modo:

 eval { do_something_that_might_barf(); }; if ( my $err = [email protected] ) { # handle $err here } 

Esistono diversi moduli CPAN che forniscono zucchero sintattico per aggiungere parole chiave try / catch e per consentire la dichiarazione semplice delle gerarchie di classi di eccezioni e quant’altro.

Il problema principale che vedo con il sistema di eccezioni di Perl è l’uso dello speciale [email protected] globale per contenere l’errore corrente, piuttosto che un meccanismo di tipo catch dedicato che potrebbe essere più sicuro, dal punto di vista dell’oscilloscopio, anche se non mi sono mai imbattuto personalmente qualsiasi problema con [email protected] che si fa male.

Alcune classi di eccezioni, ad esempio Error , non possono gestire il controllo del stream da blocchi try / catch. Questo porta a errori sottili:

 use strict; use warnings; use Error qw(:try); foreach my $blah (@somelist) { try { somemethod($blah); } catch Error with { my $exception = shift; warn "error while processing $blah: " . $exception->stacktrace(); next; # bzzt, this will not do what you want it to!!! }; # do more stuff... } 

La soluzione alternativa consiste nell’utilizzare una variabile di stato e controllare che all’esterno del blocco try / catch, che a me assomiglia terribilmente al codice n00b puzzolente.

Altri due “trucchi” in Errore (entrambi mi hanno causato dolore perché sono orribili da eseguire il debug se non ci si è già imbattuti in questo):

 use strict; use warnings; try { # do something } catch Error with { # handle the exception } 

Sembra ragionevole, giusto? Questo codice viene compilato, ma porta a errori bizzarri e imprevedibili. I problemi sono:

  1. use Error qw(:try) stato omesso, quindi il blocco try {}... sarà misparizzato (potresti visualizzare o meno un avviso, a seconda del resto del codice)
  2. manca il punto e virgola dopo il blocco catch! Non intuitivo in quanto i blocchi di controllo non usano il punto e virgola, ma in effetti si tratta di una chiamata al metodo prototipo .

Oh sì, questo mi ricorda anche che, perché try , catch ecc sono chiamate di metodo, il che significa che lo stack di chiamate all’interno di quei blocchi non sarà quello che ti aspetti. (In realtà ci sono due livelli extra di stack a causa di una chiamata interna all’interno di Error.pm.) Di conseguenza, ho alcuni moduli pieni di codice boilerplate come questo, che aggiunge solo confusione:

 my $errorString; try { $x->do_something(); if ($x->failure()) { $errorString = 'some diagnostic string'; return; # break out of try block } do_more_stuff(); } catch Error with { my $exception = shift; $errorString = $exception->text(); } finally { local $Carp::CarpLevel += 2; croak "Could not perform action blah on " . $x->name() . ": " . $errorString if $errorString; }; 

Try :: Tiny (oi moduli costruiti su di esso) è l’unico modo corretto per gestire le eccezioni in Perl 5. I problemi coinvolti sono sottili, ma l’articolo collegato li spiega in dettaglio.

Ecco come usarlo:

 use Try::Tiny; try { my $code = 'goes here'; succeed() or die 'with an error'; } catch { say "OH NOES, YOUR PROGRAM HAZ ERROR: $_"; }; 

eval e [email protected] sono parti in movimento di cui non ti devi preoccupare.

Alcune persone pensano che si tratti di un kludge, ma dopo aver letto le implementazioni di altri linguaggi (oltre a Perl 5), non è diverso da nessun altro. C’è solo la parte mobile [email protected] cui puoi catturare la tua mano … ma come con altri pezzi di macchinari con parti mobili scoperte … se non la tocchi, non ti strapperà le dita. Quindi usa Try :: Tiny e mantieni la velocità di digitazione;)

Il metodo tipico che molte persone hanno imparato a gestire le eccezioni è vulnerabile alle eccezioni intercettate mancanti:

 eval { some code here }; if( [email protected] ) { handle exception here }; 

Tu puoi fare:

 eval { some code here; 1 } or do { handle exception here }; 

Questo protegge dall’assenza dell’eccezione dovuta al fatto che [email protected] è danneggiato, ma è comunque vulnerabile a perdere il valore di [email protected] .

Per essere sicuri di non clobere un’eccezione, quando esegui la valutazione, devi localizzare [email protected] ;

 eval { local [email protected]; some code here; 1 } or do { handle exception here }; 

Questa è una rottura sottile, e la prevenzione richiede un sacco di regole esoteriche.

Nella maggior parte dei casi questo non è un problema. Ma sono stato bruciato dall’eccezione mangiando oggetti distruttori in codice reale. Il debug del problema è stato terribile.

La situazione è chiaramente negativa. Guarda tutti i moduli su CPAN costruiti offrono una gestione delle eccezioni decente.

Le risposte travolgenti a favore di Try :: Tiny combinate con il fatto che Try :: Tiny non è “troppo intelligente per metà”, mi hanno convinto a provarlo. Cose come TryCatch ed Exception :: Class :: TryCatch , Error e on and on sono troppo complesse per farmi affidare. Prova :: Tiny è un passo nella giusta direzione, ma non ho ancora una class di eccezioni leggera da usare.

Un problema che ho incontrato recentemente con il meccanismo di eccezione eval ha a che fare con il gestore $SIG{__DIE__} . Avevo – erroneamente – assunto che questo gestore venisse chiamato solo quando l’interprete Perl fosse uscito attraverso die() e volesse usare questo gestore per registrare eventi fatali. Si è poi scoperto che stavo registrando le eccezioni nel codice della libreria come errori fatali che chiaramente erano sbagliati.

La soluzione era verificare lo stato della variabile $^S o $EXCEPTIONS_BEING_CAUGHT :

 use English; $SIG{__DIE__} = sub { if (!$EXCEPTION_BEING_CAUGHT) { # fatal logging code here } }; 

Il problema che vedo qui è che il gestore __DIE__ viene utilizzato in due situazioni simili ma diverse. Quella variabile $^S sembra davvero un’aggiunta tardiva. Non so se questo è davvero il caso, però.

Con Perl, vengono combinate la lingua e le eccezioni scritte dall’utente: entrambe impostano [email protected] . In altre lingue, le eccezioni della lingua sono separate dalle eccezioni scritte dall’utente e creano un stream completamente separato.

Puoi cogliere la base delle eccezioni scritte dall’utente.

Se c’è My::Exception::one e My::Exception::two

 if ([email protected] and [email protected]>isa('My::Exception')) 

prenderà entrambi.

Ricorda di rilevare eventuali eccezioni non utente con un else .

 elsif ([email protected]) { print "Other Error [email protected]\n"; exit; } 

È anche bello avvolgere l’eccezione in una sottochiamata per lanciarla.

In C ++ e C #, puoi definire i tipi che possono essere lanciati, con blocchi catch separati che gestiscono ciascun tipo. I sistemi di tipo Perl hanno alcune problematiche relative a RTTI e ereditarietà, secondo quanto ho letto sul blog di Chomatic.

Non sono sicuro di come altri linguaggi dinamici gestiscono le eccezioni; sia C ++ che C # sono linguaggi statici e hanno un certo potere nel sistema di tipi.

Il problema filosofico è che le eccezioni di Perl 5 sono imbullonate; non sono costruiti dall’inizio del linguaggio design come qualcosa di integrale su come è scritto Perl.

È stato un lungo periodo da quando ho usato Perl, quindi la mia memoria potrebbe essere sfocata e / o Perl potrebbe essere migliorata, ma da quello che ricordo (in confronto a Python, che uso quotidianamente):

  1. poiché le eccezioni sono un’aggiunta tardiva, non sono coerentemente supportate nelle librerie principali

    (Non vero: non sono coerentemente supportati nelle librerie principali perché i programmatori che hanno scritto quelle librerie non amano le eccezioni).

  2. non esiste una gerarchia predefinita di eccezioni: non è ansible catturare un gruppo correlato di eccezioni catturando la class base

  3. non esiste un equivalente di try: … finally: … per definire il codice che verrà chiamato indipendentemente dal fatto che sia stata sollevata o meno un’eccezione, ad esempio per liberare risorse.

    ( finally in Perl è in gran parte inutile: i distruttori degli oggetti vengono eseguiti immediatamente dopo l’uscita dallo scope, non ogni volta che si verifica una pressione della memoria. In questo modo è ansible deallocare eventuali risorse non di memoria nel distruttore e funzionerà in modo corretto).

  4. (per quanto posso dire) puoi solo lanciare stringhe – non puoi lanciare oggetti che hanno informazioni aggiuntive

    (Completamente falso. die $object funziona altrettanto bene di die $string .)

  5. non puoi ottenere una traccia dello stack che ti mostri dove è stata lanciata l’eccezione – in python ottieni informazioni dettagliate incluso il codice sorgente per ogni riga nello stack di chiamate

    (Falso. perl -MCarp::Always e divertiti.)

  6. è un brutto scherzo.

    (Soggettivo: è implementato allo stesso modo in Perl come in qualsiasi altra parte, ma utilizza parole chiave con nomi diversi).

Non utilizzare Eccezioni per errori regolari. Solo i problemi fatali che interromperanno l’esecuzione corrente dovrebbero morire. Tutti gli altri dovrebbero essere maneggiati senza die .

Esempio: validazione del parametro del sub chiamato: non morire al primo problema. Controllare tutti gli altri parametri e quindi decidere di fermarsi restituendo qualcosa o avvisare e correggere i parametri difettosi e procedere. Che fanno in modalità test o di sviluppo. Ma forse die in modalità produzione. Lascia che l’applicazione decida questo.

JPR (il mio login CPAN)

Saluti da Sögel, Germania