Domanda

Quando si programma per contratto una funzione o un metodo verifica innanzitutto se le sue condizioni preliminari sono soddisfatte, prima di iniziare a lavorare sulle sue responsabilità, giusto? I due modi più importanti per eseguire questi controlli sono assert e exception .

  1. assert fallisce solo in modalità debug. Per assicurarsi che sia fondamentale testare (unità) tutte le condizioni preliminari del contratto separate per vedere se falliscono effettivamente.
  2. eccezione non riuscita in modalità debug e rilascio. Questo ha il vantaggio che il comportamento di debug testato è identico al comportamento di rilascio, ma comporta una penalità delle prestazioni di runtime.

Quale pensi sia preferibile?

Vedi la domanda ripetuta qui

È stato utile?

Soluzione

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

Quindi usa le eccezioni e usale bene. Usa una gerarchia di eccezioni valida e solida e assicurati di catturare e puoi agganciare il 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 di andare.

Altri suggerimenti

La regola empirica è che dovresti usare asserzioni quando stai cercando di cogliere i tuoi errori ed eccezioni quando cerchi di cogliere gli errori di altre persone. In altre parole, è necessario utilizzare le eccezioni per verificare i presupposti per le funzioni dell'API pubblica e ogni volta che si ottengono dati esterni al sistema. È necessario utilizzare assert per le funzioni o i dati interni al sistema.

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

Le asserzioni servono a garantire il rispetto del Contratto. Il contratto deve essere equo, quindi il cliente deve essere in grado di garantirne la conformità. Ad esempio, puoi dichiarare 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 riguardano situazioni che esulano dal controllo sia del client che del server. Un'eccezione significa che qualcosa è andato storto e non c'è nulla che si possa fare per evitarlo. Ad esempio, la connettività di rete è al di fuori del controllo delle applicazioni, pertanto non è possibile fare nulla per evitare un errore di rete.

Vorrei aggiungere che la distinzione Assertion / Exception non è davvero il modo migliore di 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 impone il contratto e viene generata un'eccezione se non è valida. Un metodo con un parametro URL è molto più chiaro di un metodo con un parametro String e un'asserzione che specifica un URL.

Gli assegni sono per catturare qualcosa che uno sviluppatore ha fatto di male (non solo te stesso - anche un altro sviluppatore nel tuo team). Se è ragionevole che un errore dell'utente possa creare questa condizione, dovrebbe essere un'eccezione.

Allo stesso modo, pensa alle conseguenze. Un'asserzione in genere arresta l'app. Se c'è qualche aspettativa realistica da cui la condizione potrebbe essere recuperata, probabilmente dovresti usare un'eccezione.

D'altra parte, se il problema può essere solo dovuto a un errore del programmatore, utilizzare un'asserzione, perché si desidera conoscerlo al più presto. Un'eccezione potrebbe essere catturata e gestita e non lo scopriresti mai. E sì, dovresti disabilitare le affermazioni nel codice di rilascio perché lì vuoi che l'app si ripristini se c'è la minima possibilità che possa. Anche se lo stato del tuo programma è profondamente rotto, l'utente potrebbe essere in grado di salvare il proprio lavoro.

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

In Costruzione di software orientato agli oggetti, 2a edizione di Bertrand Meyer, l'autore lascia una porta aperta per verificare i presupposti 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'è recupero dalla situazione: si potrebbe fare qualcosa di utile, ma è generare automaticamente un rapporto di errore e, in alcuni casi, riavviare l'applicazione.

La motivazione alla base di ciò è che i presupposti sono generalmente più economici da testare rispetto agli invarianti e ai postcondizioni, e che in alcuni casi la correttezza e la "sicurezza"; nella versione di rilascio sono più importanti della velocità. cioè per molte applicazioni la velocità non è un problema, ma robustezza (la capacità del programma di comportarsi in modo sicuro quando il suo comportamento non è corretto, cioè quando un contratto è rotto) lo è.

Dovresti sempre lasciare abilitati i controlli preliminari? Dipende. Tocca a voi. Non esiste una risposta universale. Se stai realizzando un software per una banca, potrebbe essere meglio interrompere l'esecuzione con un messaggio allarmante piuttosto che trasferire $ 1.000.000 anziché $ 1.000. E 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 catturato (perché non sono abilitate), sfortuna.

In entrambi i casi dovresti aver individuato il bug durante il test e dovresti eseguire una parte significativa del 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 ottenere comunque le eccezioni automaticamente , se le lasci abilitate, almeno a Eiffel. Penso che per fare lo stesso in C ++ devi scriverlo tu stesso.

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

C'è stato un enorme thread per quanto riguarda l'abilitazione / disabilitazione delle asserzioni nelle build di rilascio su comp.lang.c ++. moderato, che se hai qualche settimana puoi vedere quanto sono varie le opinioni al riguardo. :)

Contrariamente a coppro , I credi che se non sei sicuro che un'asserzione possa essere disabilitata in una build di rilascio, non dovrebbe essere stata un'asserzione. Le affermazioni servono a proteggere dagli invarianti del programma che vengono infranti. In tal caso, per quanto riguarda il client del tuo codice, ci saranno due possibili esiti:

  1. Muori con una sorta di errore del tipo di sistema operativo, con conseguente interruzione della chiamata. (Senza affermazione)
  2. Muori tramite una chiamata diretta per interrompere. (Con asserzione)

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

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

Personalmente, tuttavia, non sono sicuro che andrei con eccezioni anche per questo caso. Le eccezioni sono più adatte a dove può avvenire una forma di recupero adeguata. Ad esempio, è possibile che si stia tentando di allocare memoria. Quando si rileva un'eccezione 'std :: bad_alloc', potrebbe essere possibile liberare memoria e riprovare.

Ho delineato la mia opinione sullo stato della questione qui: Come convalidare lo stato interno di un oggetto? . In genere, fai valere le tue richieste e lancia violazioni da parte di altri. Per disabilitare gli asserti nelle build di rilascio, puoi fare:

  • Disabilita le asserzioni per controlli costosi (come verificare se un intervallo è ordinato)
  • Mantieni abilitati i controlli banali (come cercare un puntatore nullo o un valore booleano)

Naturalmente, nelle build di rilascio, le asserzioni non riuscite e le eccezioni non rilevate dovrebbero essere gestite in un modo diverso rispetto alle build di debug (dove potrebbe semplicemente chiamare std :: abort). Scrivi un registro dell'errore da qualche parte (possibilmente in un file), comunica al cliente che si è verificato un errore interno. Il cliente sarà in grado di inviarti il ??file di registro.

stai chiedendo la differenza tra errori di progettazione e errori di runtime.

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

le eccezioni sono le notifiche 'hey user, 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 utilizza l'app.

Quindi, se pensi di poter eliminare tutti i tuoi bug, usa solo le eccezioni. Se pensi di non poter ..... usare le eccezioni. Puoi comunque utilizzare gli assegni di debug per ridurre il numero di eccezioni ovviamente.

Non dimenticare che molte delle condizioni preliminari saranno dati forniti dall'utente, quindi avrai bisogno di un buon modo per informare l'utente che i suoi dati non erano validi. Per fare ciò, spesso dovrai restituire i dati di errore nello stack di chiamate ai bit con cui interagisce. Le asserzioni non saranno utili allora - doppiamente se l'app è di livello n.

Infine, non userei nessuno dei due: i codici di errore sono di gran lunga superiori per gli errori che ritieni possano verificarsi regolarmente. :)

Preferisco il secondo. Mentre i tuoi test potrebbero aver funzionato bene, Murphy afferma che qualcosa di inaspettato andrà storto. Quindi, invece di ottenere un'eccezione all'effettiva chiamata del metodo errata, si finisce per tracciare un frame di stack 10 NullPointerException (o equivalente) più profondo.

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

Se ritieni che sia probabile una violazione di tale presupposto, tienilo come un'eccezione o rifatti il ??presupposto.

Dovresti usare entrambi. Gli asserti sono per tua comodità come sviluppatore. Le eccezioni catturano cose che hai perso o che non ti aspettavi durante il runtime.

Mi sono appassionato di funzioni di segnalazione errori di glib invece di semplici affermazioni. Si comportano come affermazioni affermative ma invece di arrestare il programma, restituiscono solo 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 "ciò che dovrebbe". Se si arresta in modo anomalo, sai che il tuo controllo degli errori è debole da qualche altra parte lungo la strada.

Nel mio ultimo progetto, ho usato questo stile di funzioni per implementare il controllo delle condizioni preliminari e, se una di esse falliva, stampavo una traccia dello stack nel file di registro ma continuavo a funzionare. Mi ha risparmiato 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 runtime degli argomenti, farei questo:

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;
}

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

Usa le asserzioni per i casi in cui desideri disabilitarlo in produzione, errando nel lasciarli dentro. L'unico vero motivo per disabilitare in produzione, ma non in fase di sviluppo, è velocizzare il programma. Nella maggior parte dei casi, questa accelerazione non sarà significativa, ma a volte il codice è critico in termini di tempo o il test è costoso dal punto di vista computazionale. Se il codice è mission-critical, allora 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 il recupero. Ad esempio, il codice viene raramente progettato per il ripristino da errori di programmazione, ma è progettato per il ripristino da fattori quali guasti di rete o file bloccati. Gli errori non dovrebbero essere gestiti come eccezioni semplicemente per essere al di fuori del controllo del programmatore. Piuttosto, la prevedibilità di questi errori, rispetto agli errori di codifica, li rende più suscettibili al recupero.

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

Vedi anche questa domanda :

  

In alcuni casi, le affermazioni sono disabilitate quando si crea per il rilascio. Potresti   non avere il controllo su questo (altrimenti, potresti costruire con assert   on), quindi potrebbe essere una buona idea farlo in questo modo.

     

Il problema con " correzione " i valori di input è che lo farà il chiamante   non ottenere ciò che si aspettano, e questo può portare a problemi o addirittura   si arresta in modo anomalo in diverse parti del programma, facendo il debug a   incubo.

     

Solitamente lancio un'eccezione nell'istruzione if per assumere il ruolo   dell'asserzione nel caso in cui siano disabilitati

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff

La regola empirica, secondo me, è quella di usare espressioni assertive per trovare errori interni ed eccezioni per errori esterni. Puoi trarre grandi benefici 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 è d'accordo con le ipotesi del programma. Tieni presente che uno "stato definito" per un programma non è necessario essere uno "stato ideale" o persino "uno stato usuale", o persino uno "stato utile" ma più avanti su questo importante punto in seguito.

     

Per capire come le asserzioni si adattano a un programma, considera una routine in   un programma C ++ che sta per dereferenziare un puntatore. Ora dovrebbe il   test di routine se il puntatore è NULL prima della dereferenziazione, oppure   dovrebbe affermare che il puntatore non è NULL e quindi andare avanti e   dereference a prescindere?

     

Immagino che la maggior parte degli sviluppatori vorrebbe fare entrambe le cose, aggiungere l'asserzione,   ma controlla anche il puntatore per un valore NULL, al fine di non bloccarlo   qualora la condizione asserita fallisse. In superficie, eseguendo entrambi i   test e il controllo può sembrare la decisione più saggia

     

A differenza delle 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   tentando di accedere a un account senza digitare una password. E   anche se l'errore potrebbe impedire il corretto completamento del programma   compito, non vi è alcun errore del programma. Il programma non riesce ad accedere all'utente   senza password a causa di un errore esterno - un errore per l'utente   parte. Se le circostanze erano diverse e l'utente ha digitato il   password corretta e il programma non l'ha riconosciuta; poi anche se   il risultato sarebbe sempre lo stesso, il fallimento ora appartiene   il programma.

     

Lo scopo della gestione degli errori (eccezioni) è duplice. Il primo è di comunicare   all'utente (o qualche altro client) che ha un errore nell'input del programma   stato rilevato e cosa significa. Il secondo obiettivo è ripristinare   applicazione dopo il rilevamento dell'errore, a uno stato ben definito. Nota   che il programma stesso non è in errore in questa situazione. Concesso, il   il programma può trovarsi in uno stato non ideale o addirittura in uno stato in cui può farlo   niente di utile, ma non c'è errore di programmazione. Anzi,   poiché lo stato di recupero dell'errore è previsto dal programma   design, ne emette uno che il programma può gestire.

PS: potresti voler dare un'occhiata alla domanda simile: Eccezione contro asserzione .

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top