Domanda

Sono un ingegnere informatico professionista da circa un anno, dopo essermi laureato in CS. Ho saputo delle asserzioni per un po 'in C ++ e C, ma non avevo idea che esistessero in C # e .NET fino a poco tempo fa.

Il nostro codice di produzione non contiene alcuna affermazione e la mia domanda è questa ...

Dovrei iniziare a usare Assert nel nostro codice di produzione? E se è così, quando è il suo uso più appropriato? Avrebbe più senso farlo

Debug.Assert(val != null);

o

if ( val == null )
    throw new exception();
È stato utile?

Soluzione

In Debug delle applicazioni Microsoft .NET 2.0 John Robbins ha una grande sezione su affermazioni. I suoi punti principali sono:

  1. Asserisci liberamente. Non puoi mai avere troppe asserzioni.
  2. Le asserzioni non sostituiscono le eccezioni. Le eccezioni riguardano le cose richieste dal codice; le affermazioni coprono le cose che assume.
  3. Un'affermazione ben scritta può dirti non solo cosa è successo e dove (come un'eccezione), ma perché.
  4. Un messaggio di eccezione può spesso essere criptico, richiedendo di lavorare all'indietro attraverso il codice per ricreare il contesto che ha causato l'errore. Un'asserzione può preservare lo stato del programma nel momento in cui si è verificato l'errore.
  5. Le asserzioni raddoppiano come documentazione, dicendo agli altri sviluppatori da quali presupposti impliciti dipende il tuo codice.
  6. La finestra di dialogo che appare quando un'asserzione fallisce ti consente di collegare un debugger al processo, in modo da poter muovere lo stack come se avessi messo un punto di interruzione lì.

PS: se ti è piaciuto il codice completo, ti consiglio di seguirlo con questo libro. L'ho comprato per sapere come usare WinDBG e file di dump, ma la prima metà è piena di suggerimenti per aiutare a evitare bug in primo luogo.

Altri suggerimenti

Inserisci Debug.Assert () ovunque nel codice in cui desideri avere controlli di integrità per garantire gli invarianti. Quando compili una build di rilascio (ovvero nessuna costante del compilatore DEBUG ), le chiamate a Debug.Assert () verranno rimosse e vinceranno influisce sulle prestazioni.

Dovresti comunque generare eccezioni prima di chiamare Debug.Assert () . L'asserzione si assicura solo che tutto sia come previsto mentre si sta ancora sviluppando.

Da Codice completato

  

8 Programmazione difensiva

     

8.2 Asserzioni

     

Un'asserzione è il codice utilizzato durante lo sviluppo, in genere una routine   o macro: che consente a un programma di controllarsi mentre viene eseguito. Quando un   l'affermazione è vera, ciò significa che tutto funziona come previsto.   Quando è falso, significa che ha rilevato un errore imprevisto nel file   codice. Ad esempio, se il sistema presuppone che le informazioni sui clienti   il file non avrà mai più di 50.000 record, il programma potrebbe   contiene un'asserzione che il numero di record è inferiore o uguale   a 50.000. Finché il numero di record è inferiore o uguale a   50.000, l'asserzione sarà silenziosa. Se incontra più di   50.000 registrazioni, tuttavia, "asseriranno" fortemente che esiste un   errore nel programma.

     

Le asserzioni sono particolarmente utili in programmi complessi e di grandi dimensioni   in programmi ad alta affidabilità. Consentono ai programmatori di farlo più rapidamente   eliminare ipotesi di interfaccia non corrispondenti, errori che insinuano quando   il codice viene modificato e così via.

     

Un'asserzione di solito prende due argomenti: un'espressione booleana che   descrive il presupposto che dovrebbe essere vero e un messaggio a   visualizza se non lo è.

     

(...)

     

Normalmente, non si desidera che gli utenti visualizzino i messaggi di asserzione   codice di produzione; le affermazioni sono principalmente da utilizzare durante lo sviluppo   e manutenzione. Le asserzioni sono normalmente compilate nel codice all'indirizzo   tempo di sviluppo e compilato fuori dal codice per la produzione. Durante   lo sviluppo, le asserzioni eliminano le ipotesi contraddittorie,   condizioni impreviste, valori errati passati alle routine e così via.   Durante la produzione, vengono compilati fuori dal codice in modo che il file   le affermazioni non peggiorano le prestazioni del sistema.

FWIW ... Trovo che i miei metodi pubblici tendano a usare if () {throw; } per garantire che il metodo venga chiamato correttamente. I miei metodi privati ??tendono a usare Debug.Assert () .

L'idea è che con i miei metodi privati, io sono quello sotto controllo, quindi se comincio a chiamare uno dei miei metodi privati ??con parametri errati, allora ho infranto il mio presupposto da qualche parte - dovrei non sono mai entrato in quello stato. Nella produzione, queste affermazioni private dovrebbero idealmente essere un lavoro inutile poiché dovrei mantenere il mio stato interno valido e coerente. Contrasto con i parametri dati ai metodi pubblici, che potrebbero essere chiamati da chiunque in fase di esecuzione: ho ancora bisogno di imporre i vincoli dei parametri lì generando eccezioni.

Inoltre, i miei metodi privati ??possono comunque generare eccezioni se qualcosa non funziona in fase di esecuzione (errore di rete, errore di accesso ai dati, dati errati recuperati da un servizio di terze parti, ecc.). Le mie affermazioni sono lì solo per assicurarmi di non aver infranto le mie assunzioni interne sullo stato dell'oggetto.

Utilizza assert per verificare le assunzioni degli sviluppatori e le eccezioni per verificare le assunzioni ambientali.

Se fossi in te farei:

Debug.Assert(val != null);
if ( val == null )
    throw new exception();

O per evitare il controllo ripetuto delle condizioni

if ( val == null )
{
    Debug.Assert(false,"breakpoint if val== null");
    throw new exception();
}

Se vuoi Assert nel tuo codice di produzione (ad es. build di rilascio), puoi usare Trace.Assert invece di Debug.Assert.

Questo ovviamente aggiunge un sovraccarico all'eseguibile di produzione.

Inoltre, se l'applicazione è in esecuzione in modalità interfaccia utente, la finestra di dialogo Assertion verrà visualizzata per impostazione predefinita, il che potrebbe essere un po 'sconcertante per i tuoi utenti.

È possibile ignorare questo comportamento rimuovendo DefaultTraceListener: consultare la documentazione per Trace.Listeners in MSDN.

In sintesi,

  • Usa Debug.Assert liberamente per aiutare a rilevare i bug nelle build di Debug.

  • Se si utilizza Trace.Assert in modalità interfaccia utente, probabilmente si desidera rimuovere DefaultTraceListener per evitare di sconcertare gli utenti.

  • Se la condizione che stai testando è qualcosa che la tua app non è in grado di gestire, probabilmente è meglio lanciare un'eccezione, per garantire che l'esecuzione non continui. Tieni presente che un utente può scegliere di ignorare un'asserzione.

Gli avvisi vengono utilizzati per rilevare l'errore del programmatore (tuo), non l'errore dell'utente. Dovrebbero essere utilizzati solo quando non vi è alcuna possibilità che un utente possa far scattare l'asserzione. Se stai scrivendo un'API, ad esempio, le asserzioni non dovrebbero essere utilizzate per verificare che un argomento non sia nullo in alcun metodo che un utente API potrebbe chiamare. Ma potrebbe essere utilizzato in un metodo privato non esposto come parte dell'API per affermare che il TUO codice non passa mai un argomento null quando non è previsto.

Di solito preferisco le eccezioni alle asserzioni quando non ne sono sicuro.

Per lo più mai nel mio libro. Nella stragrande maggioranza delle occasioni, se vuoi controllare se tutto è sano, lancia se non lo è.

Quello che non mi piace è il fatto che rende una build di debug funzionalmente diversa da una build di rilascio. Se un'asserzione di debug fallisce ma la funzionalità funziona in versione, allora che senso ha questo senso? È ancora meglio quando l'assertore ha lasciato l'azienda da molto tempo e nessuno conosce quella parte del codice. Quindi devi dedicare un po 'del tuo tempo ad esplorare il problema per vedere se è davvero un problema o no. Se è un problema, allora perché la persona non sta gettando in primo luogo?

Per me questo suggerisce usando Debug.Assert che stai rinviando il problema a qualcun altro, affrontalo tu stesso. Se qualcosa dovrebbe essere il caso e non lo è, allora lancia.

Immagino che ci siano probabilmente scenari critici in termini di prestazioni in cui si desidera ottimizzare le asserzioni e che sono utili lì, tuttavia non ho ancora incontrato un tale scenario.

In breve

Assert sono usati per le guardie e per controllare i vincoli di Design by Contract, vale a dire:

  • Assert dovrebbe essere solo per build di debug e non di produzione. Gli asserti vengono generalmente ignorati dal compilatore nelle build di rilascio.
  • Assert può verificare la presenza di bug / condizioni impreviste che SONO nel controllo del tuo sistema
  • Assert NON è un meccanismo per la convalida di prima linea dell'input dell'utente o delle regole aziendali
  • Assert dovrebbe non essere utilizzato per rilevare condizioni ambientali impreviste (che sono al di fuori del controllo del codice) ad es. memoria insufficiente, errore di rete, errore del database, ecc. Sebbene siano rare, si prevedono queste condizioni (e il codice dell'app non può risolvere problemi come guasti hardware o esaurimento delle risorse). In genere, vengono generate eccezioni: l'applicazione può quindi intraprendere azioni correttive (ad esempio, riprovare un'operazione di database o di rete, tentare di liberare memoria memorizzata nella cache) o interrompere con garbo se l'eccezione non può essere gestita.
  • Un'asserzione fallita dovrebbe essere fatale per il tuo sistema - ad esempio, a differenza di un'eccezione, non provare a catturare o gestire Assert - il tuo codice funziona in un territorio inaspettato. Stack Traces e crash dump possono essere usati per determinare cosa è andato storto.

Le asserzioni hanno enormi vantaggi:

  • Per aiutare a trovare la mancata convalida degli input dell'utente o i bug a monte nel codice di livello superiore.
  • Le affermazioni nella base di codici trasmettono chiaramente al lettore le ipotesi formulate nel codice
  • L'asserzione verrà verificata in fase di esecuzione nelle build Debug .
  • Una volta che il codice è stato testato in modo esauriente, la ricostruzione del codice come Release rimuoverà il sovraccarico prestazionale di verifica dell'assunzione (ma con il vantaggio che una build di debug successiva ripristinerà sempre i controlli, se necessario).

... Maggiori dettagli

Debug.Assert esprime una condizione che è stata assunta sullo stato dal resto del blocco di codice all'interno del controllo del programma. Ciò può includere lo stato dei parametri forniti, lo stato dei membri di un'istanza di classe o che il ritorno da una chiamata di metodo è compreso nell'intervallo contrattato / progettato. In genere, le asserzioni dovrebbero bloccare il thread / processo / programma con tutte le informazioni necessarie (Stack Trace, Crash Dump, ecc.), Poiché indicano la presenza di un bug o una condizione non considerata per la quale non è stato progettato (ovvero non provare a catturare o gestire i fallimenti delle asserzioni), con una possibile eccezione di quando una stessa asserzione potrebbe causare più danni rispetto al bug (ad es. i controllori del traffico aereo non vorrebbero un YSOD quando un aereo diventa sottomarino, anche se è discutibile se una build di debug debba essere distribuita a produzione ...)

Quando utilizzare Assert?  - In qualsiasi punto di un sistema, API di libreria o servizio in cui gli input per una funzione o stato di una classe sono considerati validi (ad es. Quando la convalida è già stata effettuata sull'input dell'utente nel livello di presentazione di un sistema, l'azienda e le classi di livello dati in genere presuppongono che siano stati già eseguiti controlli null, controlli di intervallo, controlli di lunghezza delle stringhe ecc. sull'input).  - I controlli comuni Assert includono i casi in cui un presupposto non valido comporterebbe una dereferenza di oggetto nulla, un divisore zero, un overflow aritmetico numerico o di data e un fuori banda generale / non progettato per il comportamento (ad esempio se un 32 bit int è stato usato per modellare l'età di un essere umano, sarebbe prudente Assert che l'età sia in realtà tra 0 e 125 o giù di lì - i valori di -100 e 10 ^ 10 non sono stati progettati per).

Contratti .Net Code
Nello stack .Net, Contratti di codice possono essere utilizzati in aggiunta o in alternativa a utilizzando Debug.Assert . I contratti di codice possono ulteriormente formalizzare il controllo dello stato e possono aiutare a rilevare le violazioni delle ipotesi al momento della compilazione (o poco dopo, se eseguite come controllo in background in un IDE).

I controlli Design by Contract (DBC) disponibili includono:

  • Contract.Requires - Condizioni contrattuali
  • Contract.Ensures - PostConditions contrattate
  • Invariant - Esprime un'ipotesi sullo stato di un oggetto in tutti i punti della sua durata.
  • Contract.Assumes : pacifica il controllo statico quando viene effettuata una chiamata a metodi decorati non di contratto.

Secondo lo IDesign Standard , dovresti

  

Asserisci ogni presupposto. In media, ogni quinta riga è un'affermazione.

using System.Diagnostics;

object GetObject()
{...}

object someObject = GetObject();
Debug.Assert(someObject != null);

Come disclaimer dovrei menzionare che non ho trovato pratico implementare questo IRL. Ma questo è il loro standard.

Usa le asserzioni solo nei casi in cui desideri che il controllo venga rimosso per le build di rilascio. Ricorda, le tue asserzioni non verranno attivate se non esegui la compilazione in modalità debug.

Dato il tuo esempio check-for-null, se si trova in un'API solo interna, potrei usare un'asserzione. Se si trova in un'API pubblica, utilizzerei sicuramente il controllo esplicito e il lancio.

Tutti gli asserti dovrebbero essere codice che potrebbe essere ottimizzato per:

Debug.Assert(true);

Perché sta verificando che qualcosa che hai già assunto sia vero. Per esempio:.

public static void ConsumeEnumeration<T>(this IEnumerable<T> source)
{
  if(source != null)
    using(var en = source.GetEnumerator())
      RunThroughEnumerator(en);
}
public static T GetFirstAndConsume<T>(this IEnumerable<T> source)
{
  if(source == null)
    throw new ArgumentNullException("source");
  using(var en = source.GetEnumerator())
  {
    if(!en.MoveNext())
      throw new InvalidOperationException("Empty sequence");
    T ret = en.Current;
    RunThroughEnumerator(en);
    return ret;
  }
}
private static void RunThroughEnumerator<T>(IEnumerator<T> en)
{
  Debug.Assert(en != null);
  while(en.MoveNext());
}

In quanto sopra, ci sono tre diversi approcci ai parametri null. Il primo lo accetta come consentito (semplicemente non fa nulla). Il secondo genera un'eccezione per il codice chiamante da gestire (o meno, con conseguente messaggio di errore). Il terzo presuppone che non possa accadere, e afferma che è così.

Nel primo caso, non ci sono problemi.

Nel secondo caso, c'è un problema con il codice chiamante: non avrebbe dovuto chiamare GetFirstAndConsume con null, quindi ottiene un'eccezione.

Nel terzo caso, c'è un problema con questo codice, perché avrebbe già dovuto verificare che en! = null prima che fosse mai chiamato, quindi non è vero è un bug . O in altre parole, dovrebbe essere un codice che potrebbe teoricamente essere ottimizzato per Debug.Assert (true) , sicne en! = Null dovrebbe essere sempre true

Ho pensato di aggiungere altri quattro casi, in cui Debug.Assert può essere la scelta giusta.

1) Qualcosa che non ho visto menzionato qui è la copertura concettuale aggiuntiva che gli avvisi possono fornire durante i test automatizzati . Come semplice esempio:

Quando un chiamante di livello superiore viene modificato da un autore che ritiene di aver ampliato l'ambito del codice per gestire scenari aggiuntivi, idealmente (!) scriveranno unit test per coprire questa nuova condizione. È quindi possibile che il codice completamente integrato funzioni correttamente.

Tuttavia, in realtà è stato introdotto un sottile difetto, ma non rilevato nei risultati del test. La chiamata è diventata non deterministica in questo caso e solo accade per fornire il risultato atteso. O forse ha prodotto un errore di arrotondamento che è stato inosservato. O ha causato un errore che è stato compensato ugualmente altrove. O concesso non solo l'accesso richiesto ma privilegi aggiuntivi che non dovrebbero essere concessi. Ecc.

A questo punto, le dichiarazioni Debug.Assert () contenute nella chiamata insieme al nuovo caso (o caso limite) guidato dai test unitari possono fornire una preziosa notifica durante il test che le assunzioni dell'autore originale sono state invalidate e il codice non deve essere rilasciato senza ulteriore revisione. Le asserzioni con unit test sono i partner perfetti.

2) Inoltre, alcuni test sono semplici da scrivere, ma costosi e non necessari alla luce dei presupposti iniziali . Ad esempio:

Se è possibile accedere a un oggetto solo da un determinato punto di ingresso protetto, è necessario effettuare una query aggiuntiva su un database dei diritti di rete da ogni metodo oggetto per garantire che il chiamante disponga delle autorizzazioni? Sicuramente no. Forse la soluzione ideale include la memorizzazione nella cache o qualche altra espansione di funzionalità, ma il design non lo richiede. Un Debug.Assert () mostrerà immediatamente quando l'oggetto è stato collegato a un punto di ingresso non sicuro.

3) Successivamente, in alcuni casi il tuo prodotto potrebbe non avere alcuna utile interazione diagnostica per tutte o parte delle sue operazioni quando distribuito in modalità di rilascio . Ad esempio:

Supponiamo che sia un dispositivo incorporato in tempo reale. Generare eccezioni e riavviare quando incontra un pacchetto non valido è controproducente. Al contrario, il dispositivo può beneficiare dell'operazione di massimo sforzo, fino al punto di rendere il rumore nella sua uscita. Inoltre, potrebbe non avere un'interfaccia umana, un dispositivo di registrazione o addirittura essere fisicamente accessibile dall'essere umano quando distribuito in modalità di rilascio e la consapevolezza degli errori è meglio fornita valutando lo stesso output. In questo caso, le asserzioni liberali e i test approfonditi prima del rilascio sono più preziosi delle eccezioni.

4) Infine, alcuni test sono inutili solo perché la chiamata è percepita come estremamente affidabile . Nella maggior parte dei casi, più codice riutilizzabile è, maggiore è stato lo sforzo di renderlo affidabile. Pertanto è comune a Exception per parametri imprevisti dei chiamanti, ma asserire risultati imprevisti da callees. Ad esempio:

Se un'operazione core String.Find indica che restituirà un -1 quando non vengono trovati i criteri di ricerca, potresti essere in grado di eseguire in sicurezza un'operazione anziché tre. Tuttavia, se in realtà ha restituito -2 , potresti non avere una linea di condotta ragionevole. Sarebbe inutile sostituire il calcolo più semplice con uno che verifica separatamente un valore -1 e irragionevole nella maggior parte degli ambienti di rilascio sporcare il codice con test che assicurano che le librerie core funzionino come previsto. In questo caso gli Assert sono ideali.

Citazione tratta da Il programmatore pragmatico: da Journeyman a Master

  

Lascia asserzioni attivate

     

Esiste un malinteso comune sulle asserzioni, promulgato da   le persone che scrivono compilatori e ambienti linguistici. Va   qualcosa del genere:

     

Le asserzioni aggiungono un certo sovraccarico al codice. Perché controllano le cose   ciò non dovrebbe mai accadere, verranno attivati ??solo da un bug nel file   codice. Una volta che il codice è stato testato e spedito, non lo sono più   necessario e deve essere disattivato per rendere il codice più veloce.   Le asserzioni sono una funzione di debug.

     

Ci sono due ipotesi palesemente sbagliate qui. Innanzitutto, lo presumono   test trova tutti i bug. In realtà, per qualsiasi programma complesso tu   è improbabile che testino anche una percentuale minuscola delle permutazioni   il tuo codice verrà inserito (vedi Test spietati).

     

In secondo luogo, gli ottimisti stanno dimenticando che il programma viene eseguito in a   mondo pericoloso. Durante il test, i ratti probabilmente non rosiccheranno a   cavo di comunicazione, qualcuno che gioca non esaurisce la memoria, e   i file di registro non riempiranno il disco rigido. Queste cose potrebbero accadere quando   il programma viene eseguito in un ambiente di produzione. La tua prima linea di   difesa sta controllando la presenza di eventuali errori e il tuo secondo sta usando   affermazioni per cercare di rilevare quelli che hai perso.

     

Disattivare le asserzioni quando si consegna un programma alla produzione è   come attraversare un filo alto senza rete perché una volta ce l'hai fatta   attraverso in pratica . C'è un valore drammatico, ma è difficile ottenere la vita   assicurazione.

     

Anche se hai problemi di prestazioni, disattiva solo quelli   affermazioni che ti hanno davvero colpito .

Dovresti sempre usare il secondo approccio (gettando eccezioni).

Inoltre, se sei in produzione (e hai una build di rilascio), è meglio generare un'eccezione (e lasciare che l'app si blocchi nel peggiore dei casi) piuttosto che lavorare con valori non validi e forse distruggere i dati dei tuoi clienti (che può costare migliaia di dollari).

Dovresti usare Debug.Assert per verificare errori logici nei tuoi programmi. Il compilatore può solo informarti di errori di sintassi. Quindi dovresti assolutamente usare le istruzioni Assert per verificare errori logici. Ad esempio, testando un programma che vende auto che solo le BMW blu dovrebbero ottenere uno sconto del 15%. Il complier non può dirti nulla se il tuo programma è logicamente corretto nell'esecuzione di questo, ma un'istruzione assert potrebbe.

Ho letto le risposte qui e ho pensato di aggiungere una distinzione importante. Esistono due modi molto diversi in cui vengono utilizzate le asserzioni. Uno è come scorciatoia temporanea per gli sviluppatori per "Questo non dovrebbe davvero accadere, quindi se mi fa sapere così posso decidere cosa fare", una specie di punto di interruzione condizionale, per i casi in cui il tuo programma è in grado di continuare. L'altro è un modo per inserire nel tuo codice ipotesi su stati di programma validi.

Nel primo caso, le asserzioni non devono nemmeno essere nel codice finale. Dovresti usare Debug.Assert durante lo sviluppo e puoi rimuoverli se / quando non sono più necessari. Se vuoi lasciarli o se ti dimentichi di rimuoverli, nessun problema, dal momento che non avranno alcuna conseguenza nelle compilation di Release.

Ma nel secondo caso, le asserzioni fanno parte del codice. Asseriscono che le tue assunzioni sono vere e le documentano. In tal caso, vuoi davvero lasciarli nel codice. Se il programma è in uno stato non valido, non dovrebbe essere consentito di continuare. Se non potessi permetterti il ??colpo di prestazione non utilizzeresti C #. Da un lato, potrebbe essere utile essere in grado di collegare un debugger se accade. Dall'altro, non vuoi che la traccia dello stack venga visualizzata sui tuoi utenti e forse più importante non vuoi che possano ignorarlo. Inoltre, se è in un servizio verrà sempre ignorato. Pertanto nella produzione il comportamento corretto sarebbe quello di lanciare un'eccezione e utilizzare la normale gestione delle eccezioni del programma, che potrebbe mostrare all'utente un bel messaggio e registrare i dettagli.

Trace.Assert ha il modo perfetto per raggiungere questo obiettivo. Non verrà rimosso in produzione e può essere configurato con diversi listener utilizzando app.config. Quindi per lo sviluppo il gestore predefinito va bene, e per la produzione è possibile creare un semplice TraceListener come di seguito che genera un'eccezione e attivarlo nel file di configurazione della produzione.

using System.Diagnostics;

public class ExceptionTraceListener : DefaultTraceListener
{
    [DebuggerStepThrough]
    public override void Fail(string message, string detailMessage)
    {
        throw new AssertException(message);
    }
}

public class AssertException : Exception
{
    public AssertException(string message) : base(message) { }
}

E nel file di configurazione della produzione:

<system.diagnostics>
  <trace>
    <listeners>
      <remove name="Default"/>
      <add name="ExceptionListener" type="Namespace.ExceptionTraceListener,AssemblyName"/>
    </listeners>
  </trace>
 </system.diagnostics>

Non so come sia in C # e .NET, ma in C assert () funzionerà solo se compilato con -DDEBUG - l'utente finale non vedrà mai un assert () se è compilato senza. È solo per sviluppatori. Lo uso molto spesso, a volte è più facile rintracciare i bug.

Non li userei nel codice di produzione. Genera eccezioni, cattura e registra.

Bisogna anche fare attenzione in asp.net, poiché un'asserzione può apparire sulla console e bloccare le richieste.

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