Debug.Assert e lancio di eccezioni


92

Ho letto molti articoli (e un paio di altre domande simili che sono state pubblicate su StackOverflow) su come e quando utilizzare le asserzioni e le ho comprese bene. Ma ancora, non capisco quale tipo di motivazione dovrebbe spingermi a usare Debug.Assertinvece di lanciare una semplice eccezione. Quello che voglio dire è che in .NET la risposta predefinita a un'asserzione fallita è "fermare il mondo" e visualizzare una finestra di messaggio per l'utente. Sebbene questo tipo di comportamento possa essere modificato, trovo che sia estremamente fastidioso e ridondante farlo, mentre potrei invece semplicemente lanciare un'eccezione adeguata. In questo modo, potrei facilmente scrivere l'errore nel registro dell'applicazione appena prima di lanciare l'eccezione e, inoltre, la mia applicazione non si blocca necessariamente.

Quindi, perché dovrei, se non del tutto, usare Debug.Assertinvece di una semplice eccezione? Posizionare un'asserzione dove non dovrebbe essere potrebbe causare tutti i tipi di "comportamento indesiderato", quindi dal mio punto di vista, non guadagno davvero nulla usando un'asserzione invece di lanciare un'eccezione. Sei d'accordo con me o mi sto perdendo qualcosa qui?

Nota: capisco perfettamente qual è la differenza "in teoria" (Debug vs Release, modelli di utilizzo ecc.), Ma per come la vedo io, farei meglio a lanciare un'eccezione invece di eseguire un'asserzione. Dal momento che se viene scoperto un bug in una versione di produzione, vorrei comunque che l '"asserzione" fallisse (dopotutto, l' "overhead" è ridicolmente piccolo), quindi sarebbe meglio lanciare un'eccezione invece.


Modifica: per come la vedo io, se un'asserzione non è riuscita, significa che l'applicazione è entrata in una sorta di stato corrotto e imprevisto. Allora perché dovrei voler continuare l'esecuzione? Non importa se l'applicazione viene eseguita su una versione di debug o di rilascio. Lo stesso vale per entrambi


1
Per le cose che stai dicendo "se viene scoperto un bug su una versione di produzione, mi piacerebbe comunque che l '" affermazione "fallisse", le eccezioni sono ciò che dovresti usare
Tom Neyland,

1
Le prestazioni sono l'unica ragione. Null controllare tutto il tempo può diminuire la velocità, anche se potrebbe essere completamente impercettibile. Questo è principalmente per i casi che non dovrebbero mai accadere, ad esempio sai di averlo verificato con null già in una funzione precedente, non ha senso sprecare cicli controllandolo di nuovo. Il debug.assert agisce effettivamente come un test unitario dell'ultima possibilità per informarti.
rotola il

Risposte:


175

Anche se sono d'accordo sul fatto che il tuo ragionamento sia plausibile - cioè, se un'asserzione viene violata inaspettatamente, ha senso interrompere l'esecuzione mediante lancio - personalmente non userei eccezioni al posto delle affermazioni. Ecco perché:

Come altri hanno detto, le asserzioni dovrebbero documentare situazioni impossibili , in modo tale che se la situazione presumibilmente impossibile si verifica, lo sviluppatore viene informato. Le eccezioni, al contrario, forniscono un meccanismo di controllo del flusso per situazioni eccezionali, improbabili o errate, ma non impossibili. Per me, la differenza fondamentale è questa:

  • Dovrebbe essere SEMPRE possibile produrre un test case che eserciti una data istruzione throw. Se non è possibile produrre un tale test case, allora hai un percorso di codice nel tuo programma che non viene mai eseguito, e dovrebbe essere rimosso come codice morto.

  • Non dovrebbe MAI essere possibile produrre un test case che provochi l'attivazione di un'asserzione. Se viene generata un'asserzione, il codice è sbagliato o l'asserzione è sbagliata; in ogni caso, qualcosa deve cambiare nel codice.

Ecco perché non sostituirei un'affermazione con un'eccezione. Se l'asserzione non può effettivamente essere attivata, sostituirla con un'eccezione significa che hai un percorso di codice non verificabile nel tuo programma . Non mi piacciono i percorsi di codice non verificabili.


16
Il problema con le asserzioni è che non sono presenti nella build di produzione. Il fallimento di una condizione presunta significa che il tuo programma è entrato in un comportamento indefinito, nel qual caso un programma responsabile deve interrompere l'esecuzione il prima possibile (anche lo svolgimento dello stack è alquanto pericoloso, a seconda di quanto rigoroso vuoi ottenere). Sì, le asserzioni di solito dovrebbero essere impossibili da sparare, ma non sai cosa è possibile quando le cose si scatenano. Ciò che pensavi impossibile potrebbe accadere nella produzione e un programma responsabile dovrebbe rilevare le ipotesi violate e agire prontamente.
kizzx2

2
@ kizzx2: OK, quindi quante eccezioni impossibili per riga di codice di produzione scrivi?
Eric Lippert

4
@ kixxx2: questo è C #, quindi puoi mantenere le asserzioni nel codice di produzione utilizzando Trace.Assert. Puoi persino utilizzare il file app.config per reindirizzare le asserzioni di produzione a un file di testo piuttosto che essere scortese con l'utente finale.
HTTP 410

3
@AnorZaken: il tuo punto mostra un difetto di progettazione nelle eccezioni. Come ho notato altrove, le eccezioni sono (1) disastri fatali, (2) errori ossessivi che non dovrebbero mai accadere, (3) fallimenti di progettazione in cui un'eccezione viene utilizzata per segnalare una condizione non eccezionale o (4) condizioni esogene inaspettate . Perché queste quattro cose completamente diverse sono tutte rappresentate da eccezioni? Se avessi i miei druthers, le eccezioni "null was dereferenced" non sarebbero state rilevabili affatto . Non è mai giusto e dovrebbe terminare il tuo programma prima che faccia più danni . Dovrebbero essere più come affermazioni.
Eric Lippert

2
@Backwards_Dave: le false affermazioni sono cattive, non vere. Le asserzioni consentono di eseguire costosi controlli di verifica che non si desidera eseguire in produzione. E se un'asserzione viene violata in produzione, cosa dovrebbe fare l'utente finale al riguardo?
Eric Lippert

17

Le asserzioni vengono utilizzate per verificare la comprensione del mondo da parte del programmatore. Un'asserzione dovrebbe fallire solo se il programmatore ha fatto qualcosa di sbagliato. Ad esempio, non utilizzare mai un'asserzione per controllare l'input dell'utente.

Asserisce test per condizioni che "non possono accadere". Fanno eccezione le condizioni che "non dovrebbero accadere ma accadere".

Le asserzioni sono utili perché in fase di compilazione (o anche in fase di esecuzione) è possibile modificare il loro comportamento. Ad esempio, spesso nelle build di rilascio, le affermazioni non vengono nemmeno controllate, perché introducono un sovraccarico non necessario. Anche questo è qualcosa di cui diffidare: i tuoi test potrebbero non essere nemmeno eseguiti.

Se usi eccezioni invece di asserzioni, perdi un certo valore:

  1. Il codice è più dettagliato, poiché testare e lanciare un'eccezione è di almeno due righe, mentre un'asserzione è solo una.

  2. Il tuo codice di test e lancio verrà sempre eseguito, mentre le asserzioni possono essere compilate.

  3. Perdi un po 'di comunicazione con altri sviluppatori, perché le affermazioni hanno un significato diverso dal codice del prodotto che controlla e lancia. Se stai realmente testando un'asserzione di programmazione, usa un'asserzione.

Maggiori informazioni qui: http://nedbatchelder.com/text/assert.html


Se "non può accadere", allora perché scrivere un'asserzione. Non è ridondante? Se può effettivamente accadere ma non dovrebbe, allora non è la stessa cosa di "non dovrebbe accadere ma fare" che è per le eccezioni?
David Klempfner

2
"Non può succedere" è tra virgolette per un motivo: può accadere solo se il programmatore ha fatto qualcosa di sbagliato in un'altra parte del programma. L'assert è un controllo contro gli errori del programmatore.
Ned Batchelder

@NedBatchelder Il termine programmatore è un po 'ambiguo quando sviluppi una libreria. È giusto che quelle "situazioni impossibili" dovrebbero essere impossibili dall'utente della libreria, ma potrebbero essere possibili quando l'autore della libreria ha commesso un errore?
Bruno Zell

12

EDIT: In risposta alla modifica / nota che hai fatto nel tuo post: Sembra che l'uso delle eccezioni sia la cosa giusta da usare rispetto alle asserzioni per il tipo di cose che stai cercando di realizzare. Penso che l'ostacolo mentale che stai colpendo sia che stai considerando eccezioni e affermazioni per soddisfare lo stesso scopo, e quindi stai cercando di capire quale sarebbe 'giusto' da usare. Anche se potrebbe esserci qualche sovrapposizione nel modo in cui le asserzioni e le eccezioni possono essere utilizzate, non confondere il fatto che sono soluzioni diverse allo stesso problema, non lo sono. Le asserzioni e le eccezioni hanno ciascuna il proprio scopo, i propri punti di forza e di debolezza.

Stavo per scrivere una risposta con parole mie, ma questo rende il concetto migliore giustizia di quanto avrei avuto:

Stazione C #: asserzioni

L'uso di istruzioni assert può essere un modo efficace per rilevare gli errori della logica del programma in fase di esecuzione, e tuttavia vengono facilmente filtrati dal codice di produzione. Una volta completato lo sviluppo, il costo di runtime di questi test ridondanti per errori di codifica può essere eliminato semplicemente definendo il simbolo del preprocessore NDEBUG [che disabilita tutte le asserzioni] durante la compilazione. Assicurati, tuttavia, di ricordare che il codice inserito nell'assert stesso verrà omesso nella versione di produzione.

È preferibile utilizzare un'asserzione per testare una condizione solo quando tutte le seguenti condizioni sono valide:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

Le asserzioni non dovrebbero quasi mai essere utilizzate per rilevare situazioni che si verificano durante il normale funzionamento del software. Ad esempio, di solito le asserzioni non dovrebbero essere utilizzate per verificare la presenza di errori nell'input di un utente. Può, tuttavia, avere senso utilizzare le asserzioni per verificare che un chiamante abbia già verificato l'input di un utente.

Fondamentalmente, usa le eccezioni per le cose che devono essere catturate / gestite in un'applicazione di produzione, usa le asserzioni per eseguire controlli logici che saranno utili per lo sviluppo ma disattivati ​​in produzione.


Mi rendo conto di tutto questo. Ma il fatto è che la stessa affermazione che hai contrassegnato come grassetto vale anche per le eccezioni. Quindi per come la vedo io, invece di un'asserzione, potrei semplicemente lanciare un'eccezione (poiché se la "situazione che non dovrebbe mai verificarsi" si verifica su una versione distribuita, vorrei comunque esserne informato [più, l'applicazione potrebbe entrare in uno stato danneggiato, quindi un'eccezione è adatta, potrei non voler continuare il normale flusso di esecuzione)

1
Le asserzioni dovrebbero essere usate sulle invarianti; le eccezioni dovrebbero essere usate quando, diciamo, qualcosa non dovrebbe essere nullo, ma lo sarà (come un parametro di un metodo).
Ed S.

Immagino che tutto dipenda da quanto difensivamente vuoi codificare.
Ned Batchelder,

Sono d'accordo, per quello che sembra che tu abbia bisogno, le eccezioni sono la strada da percorrere. Hai detto che vorresti: Errori rilevati nella produzione, la capacità di registrare informazioni sugli errori e il controllo del flusso di esecuzione, ecc. Queste tre cose mi fanno pensare che quello che devi fare è aggirare alcune eccezioni.
Tom Neyland,

7

Penso che un esempio pratico (artificioso) possa aiutare a chiarire la differenza:

(adattato dall'estensione Batch di MoreLinq )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

Quindi, come hanno detto Eric Lippert e altri, asserisci solo cose che ti aspetti siano corrette, nel caso in cui tu (lo sviluppatore) le abbia usate accidentalmente in modo sbagliato da qualche altra parte, in modo da poter correggere il tuo codice. Fondamentalmente lanci eccezioni quando non hai il controllo o non puoi anticipare ciò che arriva, ad esempio per l'input dell'utente , in modo che qualsiasi cosa abbia fornito dati errati possa rispondere in modo appropriato (ad esempio l'utente).


Le tue 3 affermazioni non sono completamente ridondanti? È impossibile che i loro parametri vengano valutati su false.
David Klempfner

1
Questo è il punto: le affermazioni sono lì per documentare cose che sono impossibili. Perché dovresti farlo? Perché potresti avere qualcosa come ReSharper che ti avverte all'interno del metodo DoSomethingImpl che "potresti dereferenziare null qui" e vuoi dirgli "So cosa sto facendo, questo non può mai essere null". È anche un'indicazione per qualche programmatore successivo, che potrebbe non rendersi conto immediatamente della connessione tra DoSomething e DoSomethingImpl, specialmente se sono distanti centinaia di righe.
Marcel Popescu

4

Un'altra pepita di Code Complete :

"Un'asserzione è una funzione o una macro che si lamenta ad alta voce se un'ipotesi non è vera. Utilizza le asserzioni per documentare le ipotesi fatte nel codice e per eliminare le condizioni impreviste. ...

"Durante lo sviluppo, le asserzioni eliminano ipotesi contraddittorie, condizioni impreviste, valori errati trasmessi alle routine e così via."

Continua aggiungendo alcune linee guida su ciò che dovrebbe e non dovrebbe essere affermato.

D'altra parte, eccezioni:

"Utilizzare la gestione delle eccezioni per attirare l'attenzione su casi imprevisti. I casi eccezionali dovrebbero essere gestiti in modo da renderli evidenti durante lo sviluppo e recuperabili durante l'esecuzione del codice di produzione."

Se non hai questo libro dovresti comprarlo.


2
Ho letto il libro, è eccellente. Tuttavia .. non hai risposto alla mia domanda :)

Hai ragione, non ho risposto. La mia risposta è no, non sono d'accordo con te. Le asserzioni e le eccezioni sono animali diversi come spiegato sopra e alcune delle altre risposte pubblicate qui.
Andrew Cowenhoven,

0

Debug.Assert per impostazione predefinita funzionerà solo nelle build di debug, quindi se vuoi rilevare qualsiasi tipo di comportamento imprevisto nelle build di rilascio dovrai usare eccezioni o attivare la costante di debug nelle proprietà del tuo progetto (che è considerato in generale per non essere una buona idea).


la prima frase parziale è vera, il resto è in generale una cattiva idea: le asserzioni sono presupposti e nessuna convalida (come affermato sopra), abilitare il debug nel rilascio non è davvero un'opzione.
Marc Wittke,

0

Usa asserzioni per cose che SONO possibili ma che non dovrebbero accadere (se fosse impossibile, perché dovresti mettere un'affermazione?).

Non suona come un caso per usare un Exception? Perché dovresti usare un'affermazione invece di un Exception?

Perché dovrebbe esserci codice che viene chiamato prima della tua asserzione che impedirebbe che il parametro dell'asserzione sia falso.

Di solito non c'è codice prima del tuo Exceptionche garantisca che non verrà lanciato.

Perché è bene che Debug.Assert()sia compilato in prod? Se vuoi saperlo in debug, non vorresti saperlo in prod?

Lo vuoi solo durante lo sviluppo, perché una volta trovate le Debug.Assert(false)situazioni, scrivi il codice per garantire che Debug.Assert(false)non accada di nuovo. Una volta completato lo sviluppo, supponendo che tu abbia trovato le Debug.Assert(false)situazioni e le abbia risolte, le Debug.Assert()possono essere compilate in modo sicuro poiché ora sono ridondanti.


0

Supponi di essere un membro di un team abbastanza grande e ci sono diverse persone che lavorano tutte sulla stessa base di codice generale, inclusa la sovrapposizione di classi. È possibile creare un metodo che viene chiamato da molti altri metodi e per evitare conflitti di blocco non si aggiunge un blocco separato, ma si "presume" che sia stato precedentemente bloccato dal metodo chiamante con un blocco specifico. Ad esempio, Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Gli altri sviluppatori potrebbero trascurare un commento che dice che il metodo chiamante deve utilizzare il blocco, ma non possono ignorarlo.

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.