La gestione delle eccezioni - Exception handling

Nel calcolo e programmazione di computer , gestione delle eccezioni è il processo di rispondere al verificarsi di eccezioni - condizioni anomale o eccezionali che richiedono lavorazioni particolari - durante l' esecuzione di un programma . In generale, un'eccezione interrompe il normale flusso di esecuzione ed esegue un gestore di eccezioni preregistrato ; i dettagli su come eseguire questa operazione dipendono dal fatto che si tratti di un'eccezione hardware o software e da come viene implementata l'eccezione software. La gestione delle eccezioni, se fornita, è facilitata da costrutti di linguaggi di programmazione specializzati , meccanismi hardware come gli interrupt o strutture di comunicazione interprocesso (IPC) del sistema operativo (OS) come i segnali . Alcune eccezioni, in particolare quelle hardware, possono essere gestite con tale grazia che l'esecuzione può riprendere dal punto in cui è stata interrotta.

Un approccio alternativo per la gestione software eccezione è il controllo degli errori, che mantiene il normale flusso del programma con successivi controlli espliciti per rischi segnalate utilizzando speciali ritorno valori delle ausiliario variabile globale come C ' s errno o floating point flag di stato. Anche la convalida dell'input , che filtra preventivamente i casi eccezionali, è un approccio.

Nell'hardware

I meccanismi di eccezione hardware vengono elaborati dalla CPU. Ha lo scopo di supportare, ad esempio, il rilevamento degli errori e reindirizza il flusso del programma alle routine del servizio di gestione degli errori. Lo stato prima dell'eccezione viene salvato, ad esempio nello stack.

Gestione/trap delle eccezioni hardware: virgola mobile IEEE 754

La gestione delle eccezioni nello standard hardware in virgola mobile IEEE 754 si riferisce in generale a condizioni eccezionali e definisce un'eccezione come "un evento che si verifica quando un'operazione su alcuni operandi particolari non ha un risultato adatto per ogni applicazione ragionevole. Tale operazione potrebbe segnalare una o più eccezioni invocando il default o, se esplicitamente richiesto, una gestione alternativa definita dalla lingua."

Per default, un'eccezione IEEE 754 è ripristinabile ed è gestita sostituendo un valore predefinito per diverse eccezioni, ad esempio infinito per una divisione per eccezione allo zero, e fornendo flag di stato per la successiva verifica se è verificata l'eccezione (vedi linguaggio di programmazione C99 per un tipico esempio di gestione delle eccezioni IEEE 754). Uno stile di gestione delle eccezioni abilitato dall'uso di flag di stato comporta: prima il calcolo di un'espressione utilizzando un'implementazione rapida e diretta; verificare se ha avuto esito negativo testando i flag di stato; e poi, se necessario, chiamando un'implementazione più lenta e numericamente più robusta.

Lo standard IEEE 754 usa il termine "trapping" per riferirsi alla chiamata di una routine di gestione delle eccezioni fornita dall'utente in condizioni eccezionali ed è una caratteristica facoltativa dello standard. Lo standard raccomanda diversi scenari di utilizzo per questo, inclusa l'implementazione della pre-sostituzione non predefinita di un valore seguita dalla ripresa, per gestire in modo conciso le singolarità rimovibili .

Il comportamento predefinito di gestione delle eccezioni IEEE 754 della ripresa dopo la pre-sostituzione di un valore predefinito evita i rischi inerenti alla modifica del flusso di controllo del programma sulle eccezioni numeriche. Ad esempio, nel 1996 il volo inaugurale dell'Ariane 5 (Volo 501) si concluse con un'esplosione catastrofica dovuta in parte alla politica di gestione delle eccezioni del linguaggio di programmazione Ada di interrompere il calcolo sull'errore aritmetico, che in questo caso era una virgola mobile a 64 bit overflow di conversione di numeri interi a 16 bit . Nel caso Ariane Flight 501, i programmatori hanno protetto solo quattro delle sette variabili critiche dall'overflow a causa delle preoccupazioni sui vincoli computazionali del computer di bordo e si sono basati su quelle che si sono rivelate ipotesi errate sul possibile intervallo di valori per il tre variabili non protette perché hanno riutilizzato il codice di Ariane 4, per cui le loro ipotesi erano corrette. Secondo William Kahan , la perdita del volo 501 sarebbe stata evitata se fosse stata utilizzata la politica di sostituzione predefinita di gestione delle eccezioni IEEE 754 perché l'overflow della conversione da 64 bit a 16 bit che ha causato l'interruzione del software si è verificata in un pezzo di codice che si è rivelato completamente inutile sull'Ariane 5. Il rapporto ufficiale sull'incidente (condotto da una commissione d'inchiesta guidata da Jacques-Louis Lions ) ha osservato che "Un tema di fondo nello sviluppo di Ariane 5 è il pregiudizio verso la mitigazione di guasto casuale . Il fornitore del sistema di navigazione inerziale (SRI) stava solo seguendo la specifica fornitagli, che stabiliva che in caso di un'eccezione rilevata il processore doveva essere fermato. L'eccezione che si è verificata non era dovuta a un guasto casuale ma un errore di progettazione. L'eccezione è stata rilevata, ma gestita in modo inappropriato perché era stata presa l'opinione che il software dovesse essere considerato corretto fino a quando non viene dimostrato che è difettoso. [...] Althoug h l'errore era dovuto a un errore sistematico di progettazione del software, è possibile introdurre meccanismi per mitigare questo tipo di problema. Ad esempio, i computer all'interno degli SRI avrebbero potuto continuare a fornire le loro migliori stime delle informazioni sull'atteggiamento richieste . C'è motivo di preoccuparsi che un'eccezione software dovrebbe essere consentita, o addirittura richiesta, per causare l'arresto di un processore durante la gestione di apparecchiature mission-critical. In effetti, la perdita di una corretta funzione del software è pericolosa perché lo stesso software viene eseguito in entrambe le unità SRI. Nel caso di Ariane 501, ciò ha comportato lo spegnimento di due unità critiche ancora sane".

Dal punto di vista dell'elaborazione, gli interrupt hardware sono simili alle eccezioni ripristinabili, sebbene in genere non siano correlate al flusso di controllo del programma utente .

Strutture di gestione delle eccezioni fornite dal sistema operativo

I sistemi operativi simili a Unix forniscono funzionalità per la gestione delle eccezioni nei programmi tramite IPC . In genere, gli interrupt causati dall'esecuzione di un processo sono gestiti dalle routine del servizio di interrupt del sistema operativo e il sistema operativo può quindi inviare un segnale a quel processo, che potrebbe aver chiesto al sistema operativo di registrare un gestore di segnale da chiamare quando il segnale viene sollevato, o lasciare che il sistema operativo esegua un'azione predefinita (come terminare il programma). Esempi tipici sono SIGSEGV , SIGBUS , SIGILL e SIGFPE .

Altri sistemi operativi, ad esempio OS/360 e successori , possono utilizzare approcci diversi in sostituzione o in aggiunta a IPC.

Nel software

La gestione delle eccezioni software e il supporto fornito dagli strumenti software differiscono in qualche modo da ciò che si intende per gestione delle eccezioni nell'hardware, ma sono coinvolti concetti simili. Nei meccanismi del linguaggio di programmazione per la gestione delle eccezioni, il termine eccezione viene generalmente utilizzato in un senso specifico per indicare una struttura di dati che memorizza informazioni su una condizione eccezionale. Un meccanismo per trasferire il controllo o sollevare un'eccezione è noto come throw . Si dice che l'eccezione viene generata . L'esecuzione viene trasferita a un "catch".

Dal punto di vista dell'autore di una routine , sollevare un'eccezione è un modo utile per segnalare che una routine non può essere eseguita normalmente, ad esempio quando un argomento di input non è valido (ad es. il valore è al di fuori del dominio di una funzione ) o quando una risorsa su cui si basa non è disponibile (come un file mancante, un errore del disco rigido o errori di memoria insufficiente) o che la routine ha rilevato una condizione normale che richiede una gestione speciale, ad esempio attenzione, fine del file . Nei sistemi senza eccezioni, le routine dovrebbero restituire un codice di errore speciale . Tuttavia, questo a volte è complicato dal problema del semipredicato , in cui gli utenti della routine devono scrivere codice aggiuntivo per distinguere i valori restituiti normali da quelli errati.

I linguaggi di programmazione differiscono sostanzialmente nella loro nozione di cosa sia un'eccezione. Le lingue contemporanee possono essere grossolanamente divise in due gruppi:

  • I linguaggi in cui le eccezioni sono progettate per essere utilizzate come strutture di controllo del flusso: Ada, Modula-3, ML, OCaml, PL/I, Python e Ruby rientrano in questa categoria.
  • Linguaggi in cui le eccezioni vengono utilizzate solo per gestire situazioni anomale, imprevedibili ed errate: C++, Java, C#, Common Lisp, Eiffel e Modula-2.

Kiniry osserva inoltre che "la progettazione del linguaggio influenza solo parzialmente l'uso delle eccezioni e, di conseguenza, il modo in cui si gestiscono i guasti parziali e totali durante l'esecuzione del sistema. L'altra influenza principale sono gli esempi di utilizzo, tipicamente nelle librerie di base e negli esempi di codice in ambito tecnico libri, articoli di riviste e forum di discussione online e negli standard del codice di un'organizzazione."

Le applicazioni contemporanee devono affrontare molte sfide di progettazione quando si considerano le strategie di gestione delle eccezioni. In particolare nelle moderne applicazioni di livello aziendale, le eccezioni devono spesso attraversare i confini dei processi e dei computer. Parte della progettazione di una solida strategia di gestione delle eccezioni consiste nel riconoscere quando un processo ha fallito al punto da non poter essere gestito economicamente dalla parte software del processo.

Storia

Gestione delle eccezioni software sviluppata in Lisp negli anni '60 e '70. Questo ha avuto origine in LISP 1.5 (1962), dove le eccezioni venivano catturate dalla ERRSETparola chiave, che veniva restituita NILin caso di errore, invece di terminare il programma o entrare nel debugger. Errore innalzamento è stato introdotto nel MACLISP alla fine del 1960 tramite la ERRparola chiave. Questo è stato rapidamente utilizzato non solo per la generazione di errori, ma per il flusso di controllo non locale, e quindi è stato ampliato con due nuove parole chiave CATCHe THROW(MacLisp giugno 1972), riservando ERRSETe ERRper la gestione degli errori. Il comportamento di pulizia ora generalmente chiamato "finalmente" è stato introdotto in NIL (Nuova implementazione di LISP) tra la metà e la fine degli anni '70 come UNWIND-PROTECT. Questo è stato poi adottato da Common Lisp . Contemporaneamente a ciò era dynamic-windin Scheme, che gestiva le eccezioni nelle chiusure. I primi articoli sulla gestione strutturata delle eccezioni sono stati Goodenough (1975a) e Goodenough (1975b) . La gestione delle eccezioni è stata successivamente ampiamente adottata da molti linguaggi di programmazione dagli anni '80 in poi.

PL/I ha utilizzato eccezioni con ambito dinamico, tuttavia i linguaggi più recenti utilizzano eccezioni con ambito lessicale. La gestione delle eccezioni PL/I includeva eventi che non sono errori, ad esempio attenzione, fine del file, modifica delle variabili elencate. Sebbene alcune lingue più recenti supportino eccezioni senza errori, il loro uso non è comune.

In origine, la gestione delle eccezioni software includeva sia eccezioni ripristinabili (semantica di ripresa), come la maggior parte delle eccezioni hardware, sia eccezioni non ripristinabili (semantica di terminazione). Tuttavia, la semantica della ripresa è stata considerata inefficace nella pratica negli anni '70 e '80 (vedi discussione sulla standardizzazione C++, citata di seguito) e non è più di uso comune, sebbene fornita da linguaggi di programmazione come Common Lisp, Dylan e PL/I.

Semantica di terminazione

I meccanismi di gestione delle eccezioni nei linguaggi contemporanei sono tipicamente non ripristinabili ("semantica di terminazione") al contrario delle eccezioni hardware, che sono tipicamente ripristinabili. Ciò si basa sull'esperienza nell'uso di entrambi, poiché ci sono argomenti teorici e progettuali a favore di entrambe le decisioni; questi sono stati ampiamente dibattuti durante le discussioni sulla standardizzazione C++ 1989-1991, che hanno portato a una decisione definitiva per la semantica di terminazione. Sulla logica di un tale progetto per il meccanismo C++, Stroustrup osserva:

[N]el meeting di Palo Alto [standardizzazione C++] nel novembre 1991, abbiamo ascoltato un brillante riassunto degli argomenti per la semantica di terminazione supportato sia dall'esperienza personale che dai dati di Jim Mitchell (da Sun, precedentemente da Xerox PARC). Jim aveva utilizzato la gestione delle eccezioni in una mezza dozzina di lingue per un periodo di 20 anni ed è stato uno dei primi sostenitori della semantica della ripresa come uno dei principali progettisti e implementatori del sistema Cedar/Mesa di Xerox . Il suo messaggio era

“si preferisce la risoluzione alla ripresa; non è questione di opinioni ma di anni di esperienza. La ripresa è seducente, ma non valida".

Ha sostenuto questa affermazione con l'esperienza di diversi sistemi operativi. L'esempio chiave è stato Cedar/Mesa: è stato scritto da persone a cui piaceva e usava la ripresa, ma dopo dieci anni di utilizzo era rimasto solo un uso della ripresa nel mezzo milione di linee del sistema - e quella era un'indagine di contesto. Poiché la ripresa non era effettivamente necessaria per una tale indagine di contesto, l'hanno rimossa e hanno riscontrato un significativo aumento di velocità in quella parte del sistema. In tutti i casi in cui era stata utilizzata la ripresa, nel corso dei dieci anni era diventata un problema e una progettazione più appropriata l'aveva sostituita. In sostanza, ogni uso della ripresa aveva rappresentato un fallimento nel mantenere separati livelli di astrazione disgiunti.

Critica

Una visione contrastante sulla sicurezza della gestione delle eccezioni è stata data da Tony Hoare nel 1980, che descrive il linguaggio di programmazione Ada come avente "... pericoloso. [...] Non consentire a questo linguaggio nel suo stato attuale di essere utilizzato in applicazioni in cui l'affidabilità è fondamentale [...]. Il prossimo razzo che si perderà a causa di un errore del linguaggio di programmazione potrebbe non essere esplorativo razzo spaziale in un innocuo viaggio su Venere: potrebbe essere una testata nucleare che esplode su una delle nostre città".

La gestione delle eccezioni spesso non viene gestita correttamente nel software, specialmente quando ci sono più fonti di eccezioni; l'analisi del flusso di dati di 5 milioni di righe di codice Java ha rilevato oltre 1300 difetti di gestione delle eccezioni. Citando numerosi studi precedenti di altri (1999-2004) e i propri risultati, Weimer e Necula hanno scritto che un problema significativo con le eccezioni è che "creano percorsi di flusso di controllo nascosti su cui è difficile ragionare per i programmatori".

Go è stato inizialmente rilasciato con la gestione delle eccezioni esplicitamente omessa, con gli sviluppatori che sostenevano che offuscasse il flusso di controllo . Successivamente, al linguaggio è stato aggiunto il meccanismo di eccezione panic/ recover, che gli autori di Go consigliano di utilizzare solo per errori irreversibili che dovrebbero arrestare l'intero processo.

Le eccezioni, come il flusso non strutturato, aumentano il rischio di perdite di risorse (come l'escape di una sezione bloccata da un mutex o una che tiene temporaneamente un file aperto) o uno stato incoerente. Esistono varie tecniche per la gestione delle risorse in presenza di eccezioni, che più comunemente combinano il modello Dispos con una forma di protezione unwind (come una finallyclausola), che rilascia automaticamente la risorsa quando il controllo esce da una sezione di codice.

Supporto eccezioni nei linguaggi di programmazione

Molti linguaggi per computer hanno un supporto integrato per le eccezioni e la gestione delle eccezioni. Ciò include ActionScript , Ada , BlitzMax , C++ , C# , Clojure , COBOL , D , ECMAScript , Eiffel , Java , ML , Next Generation Shell , Object Pascal (ad es. Delphi , Free Pascal e simili), PowerBuilder , Objective-C , OCaml , PHP (a partire dalla versione 5), PL/I , PL/SQL , Prolog , Python , REALbasic , Ruby , Scala , Seed7 , Smalltalk , Tcl , Visual Prolog e la maggior parte dei linguaggi .NET . La gestione delle eccezioni in genere non è ripristinabile in quei linguaggi e, quando viene generata un'eccezione, il programma esegue la ricerca nello stack di chiamate di funzione finché non viene trovato un gestore di eccezioni.

Alcune lingue richiedono lo srotolamento dello stack man mano che la ricerca procede. Cioè, se la funzione f , contenente un gestore H per l'eccezione E , chiama la funzione g , che a sua volta chiama la funzione h , e si verifica un'eccezione E in h , allora le funzioni h e g possono essere terminate e H in f gestirà E .

I linguaggi di gestione delle eccezioni senza questo svolgimento sono Common Lisp con il suo Condition System , PL/I e Smalltalk . Tutti chiamano il gestore di eccezioni e non srotolano lo stack; tuttavia, in PL/I, se l'"unità ON" (gestore di eccezioni) fa un GOTO fuori dall'unità ON, questo svolgerà lo stack. Il gestore delle eccezioni ha la possibilità di riavviare il calcolo, riprendere o annullare. Ciò consente al programma di continuare il calcolo esattamente nello stesso punto in cui si è verificato l'errore (ad esempio quando è diventato disponibile un file precedentemente mancante) o di implementare notifiche, log, query e variabili fluide oltre al meccanismo di gestione delle eccezioni (come fatto in Smalltalk). L'implementazione stackless del linguaggio di programmazione Mythryl supporta la gestione delle eccezioni in tempo costante senza lo srotolamento dello stack.

Escludendo piccole differenze sintattiche, sono in uso solo un paio di stili di gestione delle eccezioni. Nello stile più popolare, un'eccezione viene avviata da un'istruzione speciale ( throwo raise) con un oggetto eccezione (ad es. con Java o Object Pascal) o un valore di un tipo enumerato estensibile speciale (ad es. con Ada o SML). L'ambito per i gestori di eccezioni inizia con una clausola marker ( tryo l'avviatore di blocco del linguaggio come begin) e termina all'inizio della prima clausola del gestore ( catch, except, rescue). Possono seguire diverse clausole del gestore e ciascuna può specificare quali tipi di eccezione gestisce e quale nome utilizza per l'oggetto eccezione.

Alcuni linguaggi consentono anche una clausola ( else) che viene utilizzata nel caso in cui non si sia verificata alcuna eccezione prima che sia stata raggiunta la fine dell'ambito del gestore.

Più comune è una clausola correlata ( finallyo ensure) che viene eseguita indipendentemente dal verificarsi o meno di un'eccezione, in genere per rilasciare le risorse acquisite all'interno del corpo del blocco di gestione delle eccezioni. In particolare, C++ non fornisce questo costrutto, poiché incoraggia la tecnica RAII ( Resource Acquisition Is Initialization ) che libera risorse utilizzando i distruttori .

Nel suo complesso, il codice di gestione delle eccezioni potrebbe assomigliare a questo (in pseudocodice simile a Java ):

try {
    line = console.readLine();

    if (line.length() == 0) {
        throw new EmptyLineException("The line read from console was empty!");
    }

    console.printLine("Hello %s!" % line);
    console.printLine("The program ran successfully.");
}
catch (EmptyLineException e) {
    console.printLine("Hello!");
}
catch (Exception e) {
    console.printLine("Error: " + e.message());
}
finally {
    console.printLine("The program is now terminating.");
}

Come variazione minore, alcuni linguaggi utilizzano una singola clausola del gestore, che si occupa internamente della classe dell'eccezione.

Secondo un articolo del 2008 di Westley Weimer e George Necula , la sintassi dei blocchi try... finallyin Java è un fattore che contribuisce ai difetti del software. Quando un metodo deve gestire l'acquisizione e il rilascio di 3-5 risorse, i programmatori apparentemente non sono disposti a nidificare blocchi sufficienti a causa di problemi di leggibilità, anche quando questa sarebbe una soluzione corretta. È possibile utilizzare un singolo try... finallyblocco anche quando si ha a che fare con più risorse, ma ciò richiede un uso corretto dei valori sentinella , che è un'altra fonte comune di bug per questo tipo di problema. Per quanto riguarda la semantica del costrutto try... catch... finallyin generale, Weimer e Necula scrivono che "Mentre try-catch-finally è concettualmente semplice, ha la descrizione di esecuzione più complicata nella specifica del linguaggio [Gosling et al. 1996] e richiede quattro livelli di "se" annidati nella sua descrizione inglese ufficiale. In breve, contiene un gran numero di casi limite che i programmatori spesso trascurano."

Il C supporta vari mezzi di controllo degli errori, ma generalmente non è considerato supporto per la "gestione delle eccezioni", sebbene le funzioni della libreria setjmpe longjmpstandard possano essere utilizzate per implementare la semantica delle eccezioni.

Perl ha un supporto opzionale per la gestione strutturata delle eccezioni.

Il supporto di Python per la gestione delle eccezioni è pervasivo e coerente. È difficile scrivere un programma Python robusto senza usare le sue parole chiave trye except.

Gestione delle eccezioni nelle gerarchie dell'interfaccia utente

I recenti framework Web front-end, come React e Vue , hanno introdotto meccanismi di gestione degli errori in cui gli errori si propagano lungo la gerarchia dei componenti dell'interfaccia utente, in un modo analogo a come gli errori si propagano nello stack di chiamate durante l'esecuzione del codice. Qui il meccanismo del limite di errore funge da analogo al tipico meccanismo try-catch. Pertanto, un componente può garantire che gli errori dei suoi componenti figlio vengano rilevati e gestiti e non propagati ai componenti padre.

Ad esempio, in Vue, un componente potrebbe rilevare errori implementando errorCaptured

Vue.component('parent', {
    template: '<div><slot></slot></div>',
    errorCaptured: (err, vm, info) => alert('An error occurred');
})
Vue.component('child', {
    template: '<div>{{ cause_error() }}</div>'
})

Se usato in questo modo nel markup:

<parent>
    <child></child>
</parent>

L'errore prodotto dal componente figlio viene rilevato e gestito dal componente padre.

Implementazione della gestione delle eccezioni

L'implementazione della gestione delle eccezioni nei linguaggi di programmazione in genere implica una discreta quantità di supporto sia da un generatore di codice che dal sistema runtime che accompagna un compilatore. (E' stata l'aggiunta della gestione delle eccezioni al C++ che ha posto fine alla vita utile del compilatore C++ originale, Cfront .) Due schemi sono i più comuni. La prima, la registrazione dinamica , genera codice che aggiorna continuamente le strutture sullo stato del programma in termini di gestione delle eccezioni. Tipicamente, questo aggiunge un nuovo elemento al layout dello stack frame che sa quali gestori sono disponibili per la funzione o il metodo associato a quel frame; se viene generata un'eccezione, un puntatore nel layout indirizza il runtime al codice del gestore appropriato. Questo approccio è compatto in termini di spazio, ma aggiunge un sovraccarico di esecuzione all'ingresso e all'uscita del frame. Era comunemente usato in molte implementazioni di Ada, ad esempio, dove era già necessario il supporto di generazione e runtime complessi per molte altre funzionalità del linguaggio. La registrazione dinamica, essendo abbastanza semplice da definire, è suscettibile di prova di correttezza .

Il secondo schema, e quello implementato in molti compilatori C++ di qualità produttiva, è un approccio guidato da tabelle . Ciò crea tabelle statiche in fase di compilazione e in fase di collegamento che mettono in relazione gli intervalli del contatore del programma con lo stato del programma rispetto alla gestione delle eccezioni. Quindi, se viene generata un'eccezione, il sistema di runtime cerca la posizione corrente dell'istruzione nelle tabelle e determina quali gestori sono in gioco e cosa deve essere fatto. Questo approccio riduce al minimo il sovraccarico esecutivo nel caso in cui non venga generata un'eccezione. Ciò avviene al costo di un po' di spazio, ma questo spazio può essere allocato in sezioni di dati di sola lettura per scopi speciali che non vengono caricate o riposizionate fino a quando non viene effettivamente generata un'eccezione. Anche questo secondo approccio è superiore in termini di sicurezza dei thread .

Sono stati proposti anche altri schemi di definizione e attuazione. Per i linguaggi che supportano la metaprogrammazione , sono stati avanzati approcci che non comportano alcun sovraccarico (oltre al già presente supporto per la riflessione ).

Gestione delle eccezioni basata su design by contract

Una diversa visione delle eccezioni si basa sui principi del design by contract ed è supportata in particolare dal linguaggio Eiffel . L'idea è di fornire una base più rigorosa per la gestione delle eccezioni definendo precisamente cosa sia il comportamento "normale" e "anormale". Nello specifico, l'approccio si basa su due concetti:

  • Fallimento : l'incapacità di un'operazione di adempiere al suo contratto. Ad esempio, un'addizione può produrre un overflow aritmetico (non adempie al contratto di calcolo con una buona approssimazione alla somma matematica); oppure una routine potrebbe non soddisfare la sua postcondizione.
  • Eccezione : un evento anomalo che si verifica durante l'esecuzione di una routine (quella routine è il " destinatario " dell'eccezione) durante la sua esecuzione. Tale evento anomalo deriva dal fallimento di un'operazione richiamata dalla routine.

Il "principio di gestione sicura delle eccezioni" introdotto da Bertrand Meyer in Object-Oriented Software Construction sostiene che ci sono solo due modi significativi in ​​cui una routine può reagire quando si verifica un'eccezione:

  • Fallimento o "panico organizzato": la routine corregge lo stato dell'oggetto ristabilendo l'invariante (questa è la parte "organizzata"), quindi fallisce (panico), attivando un'eccezione nel chiamante (in modo che l'evento anomalo sia non ignorato).
  • Riprova: la routine riprova l'algoritmo, di solito dopo aver modificato alcuni valori in modo che il tentativo successivo abbia maggiori possibilità di successo.

In particolare, non è consentito semplicemente ignorare un'eccezione; un blocco deve essere ritentato e completato con successo oppure propagare l'eccezione al chiamante.

Ecco un esempio espresso nella sintassi Eiffel. Presuppone che una routine send_fastsia normalmente il modo migliore per inviare un messaggio, ma potrebbe non riuscire, innescando un'eccezione; in tal caso, l'algoritmo utilizza successivamente send_slow, che fallirà meno spesso. Se send_slowfallisce, l' sendintera routine dovrebbe fallire, causando un'eccezione al chiamante.

send (m: MESSAGE) is
  -- Send m through fast link, if possible, otherwise through slow link.
local
  tried_fast, tried_slow: BOOLEAN
do
  if tried_fast then
     tried_slow := True
     send_slow (m)
  else
     tried_fast := True
     send_fast (m)
  end
rescue
  if not tried_slow then
     retry
  end
end

Le variabili locali booleane sono inizializzate su False all'inizio. Se send_fastfallisce, il corpo ( doclausola) verrà eseguito di nuovo, causando l'esecuzione di send_slow. Se questa esecuzione di send_slowfallisce, la rescueclausola verrà eseguita fino alla fine con no retry(nessuna elseclausola in final if), causando il fallimento dell'intera esecuzione della routine.

Questo approccio ha il merito di definire chiaramente cosa sono i casi "normali" e "anormali": un caso anomalo, che causa un'eccezione, è quello in cui la routine non è in grado di adempiere al suo contratto. Definisce una chiara distribuzione dei ruoli: la doclausola (ente normale) ha il compito di realizzare, o tentare di raggiungere, il contratto della routine; la rescueclausola ha il compito di ristabilire il contesto e riavviare il processo, se questo ha una possibilità di successo, ma non di eseguire alcun calcolo effettivo.

Sebbene le eccezioni in Eiffel abbiano una filosofia abbastanza chiara, Kiniry (2006) critica la loro implementazione perché "Le eccezioni che fanno parte della definizione del linguaggio sono rappresentate da valori INTEGER, le eccezioni definite dallo sviluppatore da valori STRING. [...] Inoltre, perché sono valori di base e non oggetti, non hanno una semantica intrinseca oltre a quella espressa in una routine di supporto che necessariamente non può essere infallibile a causa del sovraccarico di rappresentazione in atto (ad esempio, non si possono differenziare due interi dello stesso valore)."

Eccezioni non rilevate

Se un'eccezione viene generata e non rilevata (operativamente, viene generata un'eccezione quando non è specificato alcun gestore applicabile), l'eccezione non rilevata viene gestita dal runtime; la routine che fa questo si chiama gestore di eccezioni non rilevato . Il comportamento predefinito più comune è terminare il programma e stampare un messaggio di errore sulla console, di solito includendo informazioni di debug come una rappresentazione di stringa dell'eccezione e latracciadellostack. Ciò viene spesso evitato disponendo di un gestore di livello superiore (a livello di applicazione) (ad esempio in unciclo di eventi) che rileva le eccezioni prima che raggiungano il runtime.

Si noti che anche se un'eccezione non rilevata può provocare il programma termina nel modo anomalo (il programma potrebbe non essere corretta se un'eccezione non viene catturato, in particolare non rollback parzialmente completato le operazioni, o non liberando risorse), il processo termina normalmente (assumendo che il runtime funziona correttamente), poiché il runtime (che controlla l'esecuzione del programma) può garantire l'arresto ordinato del processo.

In un programma multithread, un'eccezione non rilevata in un thread può invece comportare la chiusura di quel thread, non dell'intero processo (le eccezioni non rilevate nel gestore a livello di thread vengono rilevate dal gestore di livello superiore). Ciò è particolarmente importante per i server, dove ad esempio un servlet (in esecuzione nel proprio thread) può essere terminato senza che il server nel suo complesso sia interessato.

Questo gestore di eccezioni non rilevato predefinito può essere sovrascritto, a livello globale o per thread, ad esempio per fornire una registrazione alternativa o la segnalazione dell'utente finale di eccezioni non rilevate o per riavviare i thread che terminano a causa di un'eccezione non rilevata. Ad esempio, in Java questo viene fatto per un singolo thread tramite Thread.setUncaughtExceptionHandlere globalmente tramite Thread.setDefaultUncaughtExceptionHandler; in Python questo viene fatto modificando sys.excepthook.

Controllo statico delle eccezioni

Eccezioni controllate

I progettisti di Java hanno ideato le eccezioni controllate, che sono un insieme speciale di eccezioni. Le eccezioni controllate che un metodo può sollevare fanno parte della firma del metodo . Ad esempio, se un metodo può lanciare un IOException, deve dichiarare questo fatto in modo esplicito nella sua firma del metodo. In caso contrario, viene generato un errore in fase di compilazione.

Kiniry (2006) nota tuttavia che le librerie di Java (come lo erano nel 2006) erano spesso incoerenti nel loro approccio alla segnalazione degli errori, perché "Non tutte le situazioni errate in Java sono però rappresentate da eccezioni. Molti metodi restituiscono valori speciali che indicano un errore codificato come campo costante di classi affini."

Le eccezioni controllate sono relative ai correttori di eccezioni che esistono per il linguaggio di programmazione OCaml . Lo strumento esterno per OCaml è sia invisibile (cioè non richiede annotazioni sintattiche) che facoltativo (cioè è possibile compilare ed eseguire un programma senza aver verificato le eccezioni, sebbene questo non sia consigliato per il codice di produzione).

Il linguaggio di programmazione CLU aveva una caratteristica con l'interfaccia più vicina a quella che Java ha introdotto in seguito. Una funzione potrebbe sollevare solo eccezioni elencate nel suo tipo, ma qualsiasi eccezione che perda dalle funzioni chiamate verrebbe automaticamente trasformata nell'unica eccezione di runtime failure, invece di generare un errore in fase di compilazione. Più tardi, Modula-3 aveva una caratteristica simile. Queste funzionalità non includono il controllo del tempo di compilazione che è centrale nel concetto di eccezioni controllate e (dal 2006) non è stato incorporato nei principali linguaggi di programmazione diversi da Java.

Le prime versioni del linguaggio di programmazione C++ includevano un meccanismo opzionale per le eccezioni controllate, chiamate specifiche delle eccezioni . Per impostazione predefinita, qualsiasi funzione potrebbe generare qualsiasi eccezione, ma ciò potrebbe essere limitato da una clausola aggiunta alla firma della funzione, che specifica quali eccezioni può generare la funzione. Le specifiche di eccezione non sono state applicate in fase di compilazione. Le violazioni hanno comportato la chiamata della funzione globale . È possibile fornire una specifica di eccezione vuota, che indica che la funzione non genererà eccezioni. Questo non è stato impostato come predefinito quando la gestione delle eccezioni è stata aggiunta al linguaggio perché avrebbe richiesto troppe modifiche al codice esistente, avrebbe impedito l'interazione con il codice scritto in altri linguaggi e avrebbe indotto i programmatori a scrivere troppi gestori a livello locale livello. L'uso esplicito di specifiche di eccezione vuote potrebbe, tuttavia, consentire ai compilatori C++ di eseguire ottimizzazioni significative del codice e del layout dello stack che sono precluse quando la gestione delle eccezioni può avvenire in una funzione. Alcuni analisti consideravano difficile l'uso corretto delle specifiche delle eccezioni in C++. Questo uso delle specifiche di eccezione è stato incluso in C++03 , deprecato nello standard del linguaggio C++ 2012 ( C++11 ) ed è stato rimosso dal linguaggio in C++17 . Una funzione che non genererà eccezioni ora può essere indicata dalla parola chiave. throwstd::unexpectednoexcept

A differenza di Java, linguaggi come C# non richiedono la dichiarazione di alcun tipo di eccezione. Secondo Hanspeter Mössenböck, non distinguere tra eccezioni da chiamare (controllate) e eccezioni da non chiamare (non controllate) rende il programma scritto più conveniente, ma meno robusto, poiché un'eccezione non rilevata comporta un'interruzione con un traccia dello stack . Kiniry (2006) nota tuttavia che JDK di Java (versione 1.4.1) genera un gran numero di eccezioni non controllate: una ogni 140 righe di codice, mentre Eiffel le usa con molta più parsimonia, con una lanciata ogni 4.600 righe di codice. Kiniry scrive anche che "Come sa qualsiasi programmatore Java, il volume di try catchcodice in una tipica applicazione Java è talvolta maggiore del codice comparabile necessario per il parametro formale esplicito e il controllo del valore restituito in altri linguaggi che non hanno eccezioni controllate. Infatti, il consenso generale tra i programmatori Java in trincea è che occuparsi delle eccezioni controllate è un compito sgradevole quasi quanto scrivere la documentazione.Pertanto, molti programmatori riferiscono di "risentire" le eccezioni controllate.Ciò porta a un'abbondanza di eccezioni controllate ma ignorate. eccezioni”. Kiniry osserva inoltre che gli sviluppatori di C# apparentemente sono stati influenzati da questo tipo di esperienze utente, con la seguente citazione attribuita a loro (tramite Eric Gunnerson):

"L'esame di piccoli programmi porta alla conclusione che la richiesta di specifiche di eccezione potrebbe sia migliorare la produttività degli sviluppatori che la qualità del codice, ma l'esperienza con progetti software di grandi dimensioni suggerisce un risultato diverso: diminuzione della produttività e aumento minimo o nullo della qualità del codice".

Secondo Anders Hejlsberg c'era un accordo abbastanza ampio nel loro gruppo di progettazione per non aver controllato le eccezioni come funzionalità del linguaggio in C#. Hejlsberg ha spiegato in un'intervista che

“La clausola throws, almeno nel modo in cui è implementata in Java, non ti obbliga necessariamente a gestire le eccezioni, ma se non le gestisci, ti costringe a riconoscere con precisione quali eccezioni potrebbero passare. Richiede di catturare le eccezioni dichiarate o di inserirle nella propria clausola throws. Per aggirare questo requisito, le persone fanno cose ridicole. Ad esempio, decorano ogni metodo con "genera Eccezione". Questo annulla completamente la funzione e hai appena fatto scrivere al programmatore più robaccia insopportabile. Questo non aiuta nessuno".

Visualizzazioni sull'utilizzo

Le eccezioni controllate possono, in fase di compilazione , ridurre l'incidenza delle eccezioni non gestite che emergono in fase di esecuzione in una determinata applicazione. Le eccezioni non controllate (come gli oggetti JavaRuntimeException e Error) rimangono non gestite.

Tuttavia, le eccezioni controllate possono richiedere throwsdichiarazioni estese , rivelare dettagli di implementazione e ridurre l' incapsulamento , oppure incoraggiare la codifica di blocchi poco considerati che possono nascondere eccezioni legittime dai gestori appropriati. Considera una base di codice in crescita nel tempo. Un'interfaccia può essere dichiarata per lanciare le eccezioni X e Y. In una versione successiva del codice, se si vuole lanciare l'eccezione Z, renderebbe il nuovo codice incompatibile con gli usi precedenti. Inoltre, con il pattern dell'adattatore , in cui un corpo di codice dichiara un'interfaccia che viene poi implementata da un corpo di codice diverso in modo che il codice possa essere collegato e chiamato dal primo, il codice dell'adattatore può avere un ricco insieme di eccezioni a descrive i problemi, ma è costretto a utilizzare i tipi di eccezione dichiarati nell'interfaccia. try/catch

È possibile ridurre il numero di eccezioni dichiarate sia dichiarando una superclasse di tutte le eccezioni potenzialmente generate, sia definendo e dichiarando tipi di eccezione adatti al livello di astrazione del metodo chiamato e mappando eccezioni di livello inferiore a questi tipi, preferibilmente avvolto utilizzando il concatenamento di eccezioni per preservare la causa principale. Inoltre, è molto probabile che nell'esempio sopra dell'interfaccia che cambia sia necessario modificare anche il codice chiamante, poiché in un certo senso le eccezioni che un metodo può generare fanno comunque parte dell'interfaccia implicita del metodo.

L'utilizzo di una dichiarazione o di solito è sufficiente per soddisfare il controllo in Java. Sebbene ciò possa essere utile, essenzialmente aggira il meccanismo delle eccezioni controllate, che Oracle scoraggia. throws Exceptioncatch (Exception e)

I tipi di eccezione non controllati in genere non dovrebbero essere gestiti, tranne forse ai livelli più esterni dell'ambito. Questi spesso rappresentano scenari che non consentono il ripristino: s spesso riflettono difetti di programmazione e s generalmente rappresentano errori JVM irreversibili. Anche in un linguaggio che supporta le eccezioni controllate, ci sono casi in cui l'uso delle eccezioni controllate non è appropriato. RuntimeExceptionError

Controllo dinamico delle eccezioni

Il punto delle routine di gestione delle eccezioni è garantire che il codice possa gestire le condizioni di errore. Per stabilire che le routine di gestione delle eccezioni siano sufficientemente robuste, è necessario presentare il codice con un ampio spettro di input non validi o imprevisti, come quelli che possono essere creati tramite l'iniezione di errori software e il test di mutazione (a volte indicato anche come fuzz prova ). Uno dei tipi di software più difficili per cui scrivere routine di gestione delle eccezioni è il software di protocollo, poiché un'implementazione robusta del protocollo deve essere preparata per ricevere input che non sono conformi alle specifiche pertinenti.

Per garantire che un'analisi di regressione significativa possa essere condotta durante un processo del ciclo di vita dello sviluppo del software , qualsiasi test di gestione delle eccezioni dovrebbe essere altamente automatizzato e i casi di test devono essere generati in modo scientifico e ripetibile. Esistono diversi sistemi disponibili in commercio che eseguono tali test.

Negli ambienti del motore di runtime come Java o .NET , esistono strumenti che si collegano al motore di runtime e ogni volta che si verifica un'eccezione di interesse, registrano le informazioni di debug che esistevano in memoria al momento in cui è stata generata l'eccezione ( stack di chiamate e heap valori). Questi strumenti sono chiamati strumenti di gestione automatizzata delle eccezioni o di intercettazione degli errori e forniscono informazioni sulla "causa principale" per le eccezioni.

Sincronicità delle eccezioni

In qualche modo correlato al concetto di eccezioni controllate è la sincronicità delle eccezioni . Le eccezioni sincrone si verificano in corrispondenza di un'istruzione di programma specifica, mentre le eccezioni asincrone possono sollevarsi praticamente ovunque. Ne consegue che la gestione asincrona delle eccezioni non può essere richiesta dal compilatore. Sono anche difficili da programmare. Esempi di eventi naturalmente asincroni includono la pressione di Ctrl-C per interrompere un programma e la ricezione di un segnale come "stop" o "sospendi" da un altro thread di esecuzione .

I linguaggi di programmazione in genere si occupano di questo limitando l'asincronicità, ad esempio Java ha deprecato l'uso della sua eccezione ThreadDeath che è stata utilizzata per consentire a un thread di interromperne un altro. Possono invece esserci eccezioni semi-asincrone che vengono generate solo in posizioni adeguate del programma o in modo sincrono.

Sistemi di condizioni

Common Lisp , Dylan e Smalltalk hanno un sistema di condizioni (vedi Common Lisp Condition System ) che comprende i suddetti sistemi di gestione delle eccezioni. In quei linguaggi o ambienti l'avvento di una condizione (una "generalizzazione di un errore" secondo Kent Pitman ) implica una chiamata di funzione, e solo in ritardo nel gestore delle eccezioni può essere presa la decisione di srotolare lo stack.

Le condizioni sono una generalizzazione delle eccezioni. Quando si verifica una condizione, viene cercato e selezionato un gestore di condizioni appropriato, in ordine di stack, per gestire la condizione. Le condizioni che non rappresentano errori possono tranquillamente non essere gestite del tutto; il loro unico scopo potrebbe essere quello di diffondere suggerimenti o avvertimenti verso l'utente.

Eccezioni continuabili

Ciò è legato al cosiddetto modello di ripresa della gestione delle eccezioni, in cui alcune eccezioni sono dette continuabili : è consentito tornare all'espressione che ha segnalato un'eccezione, dopo aver intrapreso un'azione correttiva nel gestore. Il sistema delle condizioni è generalizzato così: all'interno del gestore di una condizione non seria (aka continuable exception ), è possibile saltare a punti di riavvio predefiniti (aka riavvii ) che si trovano tra l'espressione di segnalazione e il condition handler. I riavvii sono funzioni chiuse su un certo ambiente lessicale, consentendo al programmatore di riparare questo ambiente prima di uscire completamente dal gestore delle condizioni o di srotolare lo stack anche parzialmente.

Un esempio è la condizione ENDPAGE in PL/I; l'unità ON potrebbe scrivere le righe del trailer di pagina e le righe di intestazione per la pagina successiva, quindi passare per riprendere l'esecuzione del codice interrotto.

Riavvia il meccanismo separato dalla politica

La gestione delle condizioni fornisce inoltre una separazione del meccanismo dalla politica . I riavvii forniscono vari possibili meccanismi per il ripristino dall'errore, ma non selezionano quale meccanismo è appropriato in una data situazione. Questa è la provincia del gestore della condizione, che (poiché si trova nel codice di livello superiore) ha accesso a una vista più ampia.

Un esempio: supponiamo che ci sia una funzione di libreria il cui scopo è analizzare una singola voce del file syslog . Cosa dovrebbe fare questa funzione se la voce non è corretta? Non esiste una risposta corretta, perché la stessa libreria potrebbe essere distribuita in programmi per molti scopi diversi. In un browser interattivo di file di registro, la cosa giusta da fare potrebbe essere restituire la voce non analizzata, in modo che l'utente possa vederla, ma in un programma di riepilogo automatico dei registri, la cosa giusta da fare potrebbe essere fornire valori nulli per il campi illeggibili, ma interrompono con un errore, se sono state compilate troppe voci.

Vale a dire, la domanda può essere risolta solo in termini di obiettivi più ampi del programma, che non sono noti alla funzione di libreria di uso generale. Tuttavia, uscire con un messaggio di errore è solo raramente la risposta giusta. Quindi, invece di uscire semplicemente con un errore, la funzione può stabilire riavvii offrendo vari modi per continuare, ad esempio, saltare la voce di registro, fornire valori predefiniti o nulli per i campi illeggibili, chiedere all'utente i valori mancanti o per srotolare lo stack e interrompere l'elaborazione con un messaggio di errore. I riavvii offerti costituiscono i meccanismi disponibili per il recupero dall'errore; la selezione del riavvio da parte del gestore della condizione fornisce la politica .

Guarda anche

Riferimenti

link esterno