Progettare per contratto utilizzando affermazioni o eccezioni? [chiuso]


123

Quando si programma per contratto una funzione o un metodo controlla prima se le sue precondizioni sono soddisfatte, prima di iniziare a lavorare sulle sue responsabilità, giusto? I due modi più importanti per eseguire questi controlli sono di tanto assertin tanto exception.

  1. assert fallisce solo in modalità debug. Per assicurarsi che sia fondamentale testare (unitariamente) tutte le precondizioni contrattuali separate per vedere se effettivamente falliscono.
  2. l'eccezione non riesce in modalità debug e rilascio. Ciò ha il vantaggio che il comportamento di debug testato è identico al comportamento di rilascio, ma incorre in una penalizzazione delle prestazioni di runtime.

Quale pensi sia preferibile?

Vedi la domanda correlata qui


3
L'intero punto alla base della progettazione per contratto è che non è necessario (e probabilmente non si dovrebbe) verificare le precondizioni in fase di esecuzione. Verifichi l'input prima di passarlo al metodo con le precondizioni, è così che rispetti la tua fine del contratto. Se l'input non è valido o viola la fine del contratto, il programma di solito fallirà comunque durante il normale corso delle azioni (che si desidera).
void

Bella domanda, ma penso che dovresti davvero cambiare la risposta accettata (come mostrano anche i voti)!
DaveFar

Per sempre, lo so, ma questa domanda dovrebbe effettivamente avere il tag c ++? Stavo cercando questa risposta, da usare in un'altra lingua (Delpih) e non riesco a immaginare nessuna lingua che presenti eccezioni e asserzioni che non seguano le stesse regole. (Sto ancora imparando le linee guida di Stack Overflow.)
Eric G

Risposta molto succinta fornita in questa risposta : "In altre parole, le eccezioni riguardano la robustezza della tua applicazione mentre le asserzioni ne riguardano la correttezza."
Shmuel Levine,

Risposte:


39

Disabilitare l'assert nelle build di rilascio è come dire "Non avrò mai alcun problema in una build di rilascio", il che spesso non è il caso. Quindi assert non dovrebbe essere disabilitato in una build di rilascio. Ma non vuoi nemmeno che la build di rilascio si blocchi ogni volta che si verificano errori, vero?

Quindi usa le eccezioni e usale bene. Usa una buona e solida gerarchia delle eccezioni e assicurati di catturare e di poter mettere un gancio sul lancio di eccezioni nel tuo debugger per catturarlo, e in modalità di rilascio puoi compensare l'errore piuttosto che un crash diretto. È il modo più sicuro per andare.


4
Le asserzioni sono utili per lo meno nei casi in cui il controllo della correttezza sarebbe inefficiente o inefficiente da implementare correttamente.
Casebash

89
Il punto nelle asserzioni non è correggere gli errori, ma avvisare il programmatore. Mantenerli abilitati nelle build di rilascio è inutile per questo motivo: cosa avresti guadagnato con un assert firing? Lo sviluppatore non sarà in grado di eseguire il debug. Le asserzioni sono un aiuto per il debug, non sostituiscono le eccezioni (e le eccezioni non sostituiscono le asserzioni). Le eccezioni avvisano il programma di una condizione di errore. Assert avvisa lo sviluppatore.
jalf

12
Ma un'asserzione dovrebbe essere usata quando i dati interni sono stati corrotti dopo la correzione: se un'asserzione si attiva, non puoi fare supposizioni sullo stato del programma perché significa che qualcosa è / sbagliato /. Se un'asserzione è stata emessa, non puoi presumere che i dati siano validi. Ecco perché una build di rilascio dovrebbe affermare: non per dire al programmatore dove si trova il problema, ma in modo che il programma possa spegnersi e non rischiare problemi più grandi. Il programma dovrebbe fare quello che può per facilitare il recupero in un secondo momento, quando i dati possono essere considerati attendibili.
coppro

5
@jalf, sebbene non sia possibile inserire un hook nel debugger nelle build di rilascio, è possibile sfruttare la registrazione in modo che gli sviluppatori vedano le informazioni rilevanti per la mancata asserzione. In questo documento ( martinfowler.com/ieeeSoftware/failFast.pdf ), Jim Shore sottolinea: "Ricorda, un errore che si verifica presso il sito del cliente ha superato il tuo processo di test. Probabilmente avrai problemi a riprodurlo. Questi errori sono il più difficile da trovare e un'affermazione ben piazzata che spieghi il problema potrebbe farti risparmiare giorni di fatica ".
StriplingWarrior

5
Personalmente preferisco asserzioni per approcci di design by contract. Le eccezioni sono difensive e stanno facendo il controllo degli argomenti all'interno della funzione. Inoltre, le precondizioni dbc non dicono "Non funzionerò se usi valori fuori dall'intervallo di lavoro" ma "Non garantisco di fornire la risposta giusta, ma posso comunque farlo". Le affermazioni forniscono allo sviluppatore un feedback sul fatto che stanno chiamando una funzione con una violazione della condizione, ma non impedirgli di usarla se ritengono di conoscerla meglio. La violazione potrebbe causare il verificarsi di eccezioni, ma la vedo come una cosa diversa.
Matt_JD

194

La regola pratica è che dovresti usare le asserzioni quando cerchi di rilevare i tuoi errori ed eccezioni quando cerchi di rilevare gli errori di altre persone. In altre parole, dovresti utilizzare le eccezioni per verificare le condizioni preliminari per le funzioni API pubbliche e ogni volta che ottieni dati esterni al tuo sistema. È necessario utilizzare asserzioni per le funzioni o i dati interni al sistema.


che dire di serializzare / deserializzare seduti in diversi moduli / applicazioni e alla fine andare fuori sincronizzazione? Voglio dire, da parte del lettore, è sempre un mio errore se cerco di leggere le cose nel modo sbagliato, quindi tendo a usare assert, ma d'altra parte ho dati esterni, che possono eventualmente cambiare formato senza preavviso.
Slava

Se i dati sono esterni, è necessario utilizzare le eccezioni. In questo caso particolare dovresti probabilmente anche catturare quelle eccezioni e gestirle in qualche modo ragionevole, piuttosto che lasciar morire il tuo programma. Inoltre, la mia risposta è una regola pratica, non una legge di natura. :) Quindi devi considerare ogni caso individualmente.
Dima

Se la tua funzione f (int * x) contiene una riga x-> len, allora f (v) dove v è dimostrato nullo è garantito il crash. Inoltre, se anche in precedenza su v è dimostrato di essere nullo ma f (v) è stato chiamato, si ha una contraddizione logica. È lo stesso che avere a / b dove b è in definitiva dimostrato di essere 0. Idealmente, tale codice non dovrebbe essere compilato. Disattivare i controlli delle ipotesi è completamente sciocco a meno che il problema non sia il costo dei controlli, perché oscura la posizione in cui un'ipotesi è stata violata. Almeno deve essere registrato. Dovresti comunque avere un progetto di riavvio in caso di arresto anomalo.
Rob

22

Il principio che seguo è questo: se una situazione può essere realisticamente evitata codificando, usa un'asserzione. Altrimenti usa un'eccezione.

Le asserzioni servono a garantire che il Contratto venga rispettato. Il contratto deve essere equo, in modo che il cliente deve essere in grado di garantire che sia conforme. Ad esempio, puoi affermare in un contratto che un URL deve essere valido perché le regole su ciò che è e non è un URL valido sono note e coerenti.

Le eccezioni sono per le situazioni che sono al di fuori del controllo sia del client che del server. Un'eccezione significa che qualcosa è andato storto e non c'è niente che avrebbe potuto essere fatto per evitarlo. Ad esempio, la connettività di rete è al di fuori del controllo delle applicazioni, quindi non è possibile fare nulla per evitare un errore di rete.

Vorrei aggiungere che la distinzione Asserzione / Eccezione non è davvero il modo migliore per pensarci. Quello a cui vuoi davvero pensare è il contratto e come può essere applicato. Nel mio esempio di URL sopra, la cosa migliore da fare è avere una classe che incapsula un URL ed è Null o un URL valido. È la conversione di una stringa in un URL che applica il contratto e viene generata un'eccezione se non è valido. Un metodo con un parametro URL è molto più chiaro di un metodo con un parametro String e un'asserzione che specifica un URL.


6

Le affermazioni servono per cogliere qualcosa che uno sviluppatore ha fatto di sbagliato (non solo te stesso, ma anche un altro sviluppatore del tuo team). Se è ragionevole che un errore dell'utente possa creare questa condizione, allora dovrebbe essere un'eccezione.

Allo stesso modo pensa alle conseguenze. Un'asserzione in genere chiude l'app. Se esiste un'aspettativa realistica dalla quale la condizione potrebbe essere risolta, è probabilmente necessario utilizzare un'eccezione.

D'altra parte, se il problema può essere dovuto solo a un errore del programmatore, usa un'asserzione, perché vuoi saperlo il prima possibile. Un'eccezione potrebbe essere individuata e gestita e non la scopriresti mai. E sì, dovresti disabilitare le affermazioni nel codice di rilascio perché vuoi che l'app venga ripristinata se c'è la minima possibilità che possa farlo. Anche se lo stato del programma è profondamente compromesso, l'utente potrebbe essere in grado di salvare il proprio lavoro.


5

Non è esattamente vero che "l'assert fallisce solo in modalità debug".

In Object Oriented Software Construction, 2nd Edition di Bertrand Meyer, l'autore lascia una porta aperta per il controllo delle precondizioni in modalità di rilascio. In tal caso, ciò che accade quando un'asserzione fallisce è che ... viene sollevata un'eccezione di violazione dell'asserzione! In questo caso non c'è ripristino dalla situazione: si potrebbe però fare qualcosa di utile, ovvero generare automaticamente un report di errore e, in alcuni casi, riavviare l'applicazione.

La motivazione alla base di ciò è che le precondizioni sono in genere più economiche da testare rispetto alle invarianti e alle postcondizioni, e che in alcuni casi la correttezza e la "sicurezza" nella build di rilascio sono più importanti della velocità. cioè Per molte applicazioni la velocità non è un problema, ma la robustezza (la capacità del programma di comportarsi in modo sicuro quando il suo comportamento non è corretto, cioè quando un contratto è rotto) lo è.

È necessario lasciare sempre abilitati i controlli delle precondizioni? Dipende. Tocca a voi. Non esiste una risposta universale. Se stai realizzando software per una banca, potrebbe essere meglio interrompere l'esecuzione con un messaggio allarmante piuttosto che trasferire $ 1.000.000 invece di $ 1.000. Ma cosa succede se stai programmando un gioco? Forse hai bisogno di tutta la velocità che puoi ottenere e se qualcuno ottiene 1000 punti invece di 10 a causa di un bug che le condizioni preliminari non hanno rilevato (perché non sono abilitate), sfortuna.

In entrambi i casi dovresti idealmente aver individuato quel bug durante il test e dovresti fare una parte significativa del tuo test con le asserzioni abilitate. Ciò che viene discusso qui è qual è la migliore politica per quei rari casi in cui le precondizioni falliscono nel codice di produzione in uno scenario che non è stato rilevato in precedenza a causa di test incompleti.

Per riassumere, puoi avere affermazioni e comunque ottenere automaticamente le eccezioni , se le lasci abilitate, almeno in Eiffel. Penso che per fare lo stesso in C ++ devi digitarlo da solo.

Vedi anche: Quando dovrebbero rimanere le asserzioni nel codice di produzione?


1
Il tuo punto è decisamente valido. Il SO non ha specificato un linguaggio particolare: nel caso di C # l'assert standard è System.Diagnostics.Debug.Assert, che non riesce solo in una build di debug e verrà rimosso in fase di compilazione in una build di rilascio.
yoyo

2

C'era un enorme thread riguardo l'abilitazione / disabilitazione delle asserzioni nelle build di rilascio su comp.lang.c ++. Moderato, che se hai qualche settimana puoi vedere quanto siano varie le opinioni su questo. :)

Contrariamente a coppro , credo che se non sei sicuro che un'asserzione possa essere disabilitata in una build di rilascio, non avrebbe dovuto essere un'asserzione. Le asserzioni servono a proteggere dall'interruzione delle invarianti del programma. In tal caso, per quanto riguarda il client del tuo codice ci sarà uno dei due possibili risultati:

  1. Muori con una sorta di errore del tipo di sistema operativo, con conseguente richiesta di interruzione. (Senza affermare)
  2. Muori tramite una chiamata diretta per abortire. (Con affermare)

Non c'è differenza per l'utente, tuttavia, è possibile che le affermazioni aggiungano un costo di prestazioni non necessario nel codice che è presente nella stragrande maggioranza delle esecuzioni in cui il codice non fallisce.

La risposta alla domanda in realtà dipende molto di più da chi saranno i client dell'API. Se stai scrivendo una libreria che fornisce un'API, hai bisogno di una qualche forma di meccanismo per notificare ai tuoi clienti che hanno utilizzato l'API in modo errato. A meno che tu non fornisca due versioni della libreria (una con asserzioni, una senza), allora asserire è molto improbabile che sia la scelta appropriata.

Personalmente, tuttavia, non sono sicuro che avrei fatto eccezioni neanche per questo caso. Le eccezioni sono più adatte a dove può aver luogo una forma adeguata di recupero. Ad esempio, potrebbe essere che tu stia cercando di allocare memoria. Quando si rileva un'eccezione "std :: bad_alloc", potrebbe essere possibile liberare memoria e riprovare.


2

Ho delineato il mio punto di vista sullo stato della questione qui: come convalidi lo stato interno di un oggetto? . In generale, fai valere le tue affermazioni e lancia per violazione da parte di altri. Per disabilitare le asserzioni nelle build di rilascio, puoi fare:

  • Disabilita asserzioni per controlli costosi (come controllare se un intervallo è ordinato)
  • Mantieni abilitati i controlli banali (come il controllo di un puntatore nullo o un valore booleano)

Ovviamente, nelle build di rilascio, le asserzioni fallite e le eccezioni non rilevate dovrebbero essere gestite in un altro modo rispetto alle build di debug (dove potrebbe semplicemente chiamare std :: abort). Scrivi un registro dell'errore da qualche parte (possibilmente in un file), informa il cliente che si è verificato un errore interno. Il cliente potrà inviarti il ​​file di log.


1

stai chiedendo la differenza tra gli errori in fase di progettazione e quelli in fase di esecuzione.

afferma che sono notifiche 'hey programmatore, questo è rotto', sono lì per ricordarti bug che non avresti notato quando si sono verificati.

le eccezioni sono le notifiche "hey utente, qualcosa è andato storto" (ovviamente puoi programmare per catturarle in modo che l'utente non venga mai informato) ma queste sono progettate per verificarsi in fase di esecuzione quando l'utente Joe sta utilizzando l'app.

Quindi, se pensi di poter eliminare tutti i tuoi bug, usa solo le eccezioni. Se pensi di non poterlo fare ..... usa le eccezioni. È comunque possibile utilizzare le asserzioni di debug per ridurre il numero di eccezioni, ovviamente.

Non dimenticare che molte delle condizioni preliminari saranno i dati forniti dall'utente, quindi avrai bisogno di un buon modo per informare l'utente che i suoi dati non erano buoni. Per fare ciò, dovrai spesso restituire i dati di errore nello stack di chiamate ai bit con cui sta interagendo. Le affermazioni non saranno utili allora, doppiamente se la tua app è a più livelli.

Infine, non userei nessuno dei due: i codici di errore sono di gran lunga superiori per gli errori che pensi si verifichino regolarmente. :)


0

Preferisco la seconda. Anche se i tuoi test potrebbero essere andati bene, Murphy dice che qualcosa di inaspettato andrà storto. Quindi, invece di ottenere un'eccezione alla chiamata al metodo errata effettiva, si finisce per tracciare una NullPointerException (o equivalente) 10 stack frame più in profondità.


0

Le risposte precedenti sono corrette: usa le eccezioni per le funzioni API pubbliche. L'unica volta in cui potresti voler piegare questa regola è quando il controllo è costoso dal punto di vista computazionale. In tal caso, puoi metterlo in un'affermazione.

Se ritieni che sia probabile una violazione di quella precondizione, tienila come eccezione o rifattorizza la precondizione.


0

Dovresti usare entrambi. Le affermazioni sono per la tua comodità come sviluppatore. Le eccezioni rilevano cose che ti sei perso o che non ti aspettavi durante il runtime.

Mi sono appassionato alle funzioni di segnalazione degli errori di glib invece delle semplici vecchie affermazioni. Si comportano come dichiarazioni di asserzione ma invece di interrompere il programma, restituiscono semplicemente un valore e lasciano che il programma continui. Funziona sorprendentemente bene e come bonus puoi vedere cosa succede al resto del tuo programma quando una funzione non restituisce "quello che dovrebbe". Se si blocca, sai che il tuo controllo degli errori è lento da qualche altra parte lungo la strada.

Nel mio ultimo progetto, ho utilizzato questo stile di funzioni per implementare il controllo delle precondizioni e, se una di esse falliva, stampavo una traccia dello stack nel file di registro ma continuavo a funzionare. Mi ha fatto risparmiare un sacco di tempo per il debug quando altre persone avrebbero riscontrato un problema durante l'esecuzione della mia build di debug.

#ifdef DEBUG
#define RETURN_IF_FAIL(expr)      do {                      \
 if (!(expr))                                           \
 {                                                      \
     fprintf(stderr,                                        \
        "file %s: line %d (%s): precondition `%s' failed.", \
        __FILE__,                                           \
        __LINE__,                                           \
        __PRETTY_FUNCTION__,                                \
        #expr);                                             \
     ::print_stack_trace(2);                                \
     return;                                                \
 };               } while(0)
#define RETURN_VAL_IF_FAIL(expr, val)  do {                         \
 if (!(expr))                                                   \
 {                                                              \
    fprintf(stderr,                                             \
        "file %s: line %d (%s): precondition `%s' failed.",     \
        __FILE__,                                               \
        __LINE__,                                               \
        __PRETTY_FUNCTION__,                                    \
        #expr);                                                 \
     ::print_stack_trace(2);                                    \
     return val;                                                \
 };               } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif

Se avessi bisogno del controllo in runtime degli argomenti, lo farei:

char *doSomething(char *ptr)
{
    RETURN_VAL_IF_FAIL(ptr != NULL, NULL);  // same as assert(ptr != NULL), but returns NULL if it fails.
                                            // Goes away when debug off.

    if( ptr != NULL )
    {
       ...
    }

    return ptr;
}

Non credo di aver visto nella domanda OP nulla relativo a C ++. Credo che non dovrebbe essere incluso nella tua risposta.
ForceMagic

@ForceMagic: la domanda aveva il tag C ++ nel 2008 quando ho pubblicato questa risposta, e in effetti il ​​tag C ++ è stato rimosso solo 5 ore fa. Indipendentemente da ciò, il codice illustra un concetto indipendente dalla lingua.
indiv

0

Ho provato a sintetizzare molte delle altre risposte qui con le mie opinioni.

Usa le asserzioni per i casi in cui vuoi disabilitarlo in produzione, errando nel lasciarle dentro. L'unico vero motivo per disabilitare in produzione, ma non in sviluppo, è accelerare il programma. Nella maggior parte dei casi, questa velocità non sarà significativa, ma a volte il codice è critico in termini di tempo o il test è costoso in termini di calcolo. Se il codice è fondamentale per la missione, le eccezioni potrebbero essere le migliori nonostante il rallentamento.

Se esiste una reale possibilità di recupero, utilizzare un'eccezione poiché le asserzioni non sono progettate per essere ripristinate. Ad esempio, il codice è raramente progettato per il ripristino da errori di programmazione, ma è progettato per il ripristino da fattori quali errori di rete o file bloccati. Gli errori non dovrebbero essere trattati come eccezioni semplicemente perché fuori dal controllo del programmatore. Piuttosto, la prevedibilità di questi errori, rispetto agli errori di codifica, li rende più facili da recuperare.

Argomento che è più facile eseguire il debug delle asserzioni: la traccia dello stack da un'eccezione denominata correttamente è facile da leggere come un'asserzione. Un buon codice dovrebbe catturare solo tipi specifici di eccezioni, quindi le eccezioni non dovrebbero passare inosservate perché vengono catturate. Tuttavia, penso che Java a volte ti costringa a catturare tutte le eccezioni.


0

La regola pratica, per me, è che usa espressioni di asserzione per trovare errori interni ed eccezioni per errori esterni. Puoi trarre molto vantaggio dalla seguente discussione di Greg da qui .

Le espressioni di asserzione vengono utilizzate per trovare errori di programmazione: errori nella logica del programma stesso o errori nella sua implementazione corrispondente. Una condizione di asserzione verifica che il programma rimanga in uno stato definito. Uno "stato definito" è fondamentalmente uno che concorda con i presupposti del programma. Notare che uno "stato definito" per un programma non deve essere necessariamente uno "stato ideale" o anche "uno stato normale", o anche uno "stato utile", ma parleremo di questo punto importante più avanti.

Per capire come le asserzioni si adattano a un programma, si consideri una routine in un programma C ++ che sta per dereferenziare un puntatore. Ora la routine dovrebbe verificare se il puntatore è NULL prima della dereferenziazione, o dovrebbe affermare che il puntatore non è NULL e poi andare avanti e dereferenziarlo a prescindere?

Immagino che la maggior parte degli sviluppatori vorrebbe fare entrambe le cose, aggiungere l'asserzione, ma anche controllare il puntatore per un valore NULL, in modo da non andare in crash se la condizione asserita fallisce. In superficie, eseguire sia il test che il controllo può sembrare la decisione più saggia

A differenza delle sue condizioni asserite, la gestione degli errori di un programma (eccezioni) non si riferisce agli errori nel programma, ma agli input che il programma ottiene dal suo ambiente. Questi sono spesso "errori" da parte di qualcuno, come un utente che tenta di accedere a un account senza digitare una password. E anche se l'errore può impedire il completamento con successo dell'attività del programma, non si verifica alcun errore del programma. Il programma non riesce ad accedere all'utente senza una password a causa di un errore esterno, un errore da parte dell'utente. Se le circostanze erano diverse e l'utente ha digitato la password corretta e il programma non è riuscito a riconoscerla; quindi, anche se il risultato sarebbe sempre lo stesso, il fallimento ora appartiene al programma.

Lo scopo della gestione degli errori (eccezioni) è duplice. Il primo è comunicare all'utente (o qualche altro client) che è stato rilevato un errore nell'input del programma e cosa significa. Il secondo scopo è ripristinare l'applicazione dopo il rilevamento dell'errore, in uno stato ben definito. Notare che il programma stesso non è in errore in questa situazione. Certo, il programma può trovarsi in uno stato non ideale, o anche in uno stato in cui non può fare nulla di utile, ma non ci sono errori di programmazione. Al contrario, poiché lo stato di ripristino dell'errore è previsto dalla progettazione del programma, è uno stato che il programma può gestire.

PS: potresti voler controllare la domanda simile: eccezione contro asserzione .


-1

Vedi anche questa domanda :

In alcuni casi, le affermazioni sono disabilitate durante la compilazione per il rilascio. Potresti non avere il controllo su questo (altrimenti, potresti costruire con asserzioni attive), quindi potrebbe essere una buona idea farlo in questo modo.

Il problema con la "correzione" dei valori di input è che il chiamante non otterrà ciò che si aspetta, e questo può portare a problemi o addirittura ad arresti anomali in parti completamente diverse del programma, rendendo il debug un incubo.

Di solito lancio un'eccezione nell'istruzione if per assumere il ruolo di assert nel caso in cui siano disabilitati

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.