9

Sto scrivendo test di unità per un'app Web MVC e ho ricevuto eccezioni di riferimento null perché gli oggetti di test mocked-up sono solo parzialmente inizializzati. So che la linea sta gettando le eccezioni, e sembra qualcosa di simile:Visual Studio può dirmi quale riferimento ha generato una NullReferenceException?

return Supervisor.RegistrationInformation.Registrations 
    .Any(r => 
     r.RegistrationCountry.IsUSAOrCandada() && 
     (!DatesWorked.Start.HasValue || r.RegistrationDate <= DatesWorked.Start.Value) && 
     (!DatesWorked.End.HasValue || r.RegistrationExpirationDate >= DatesWorked.End.Value) && 
     //... 

Ci sono un sacco di riferimenti in là, e nessuno di loro potrebbe essere il problema. Tuttavia, lo stesso NullReferenceException non sembra catturare quale riferimento si è verificato. Il fatto che sto passando una lambda presenta un'altra sfida: per quanto posso dire, non posso passare attraverso il lambda durante il debug e vedere quali membri di r sono nulli.

C'è un modo che io possa fare una o entrambe le seguenti operazioni:

  • dispone di Visual Studio dirmi esattamente che ha gettato il riferimento NullReferenceException?
  • In mancanza di questo, c'è un modo per fare il passo debugger attraverso l'espressione lambda (o semplicemente il mouse sopra le cose per vedere i loro valori) come è in corso di valutazione da parte Any?

Mi sento come se ci fosse un modo per fare queste cose, ma non riesco a trovarlo. Sono su VS2010 Premium, e ho Resharper, VS Power Tools e un paio di altre estensioni installate. Se c'è un componente aggiuntivo che fa questo, ci starei bene.

Edit:

Come Eric Lippert sottolinea, è impossibile individuare l'origine dei un'eccezione NR quando il codice è stato compilato in configurazione di rilascio. Sto solo chiedendo di lavorare in modalità di debug. Se Visual Studio (o qualche estensione di VS) è in grado di tracciare l'origine di un riferimento durante il debug, ciò risponderebbe alla mia domanda.

Edit 2:

La seconda domanda - come rompere e passo attraverso una lambda - è stato risposto, ma mi piace ancora di sapere se c'è un modo automatico per rintracciare un riferimento null.

+0

No, non c'è modo di farlo, tranne che ReSharper può aiutare. –

+0

Hai provato a cambiare il lambda in una funzione anonima e inserire il punto di interruzione al suo interno? – kol

+0

Abilita la gestione delle eccezioni della prima possibilità (Debug => Eccezioni) e si interrompe quando viene lanciata NullReferenceException? –

risposta

17

Non c'è, in generale, un modo per fare quello che vuoi, no. Per capire perché, pensa a cosa sta accadendo quando viene lanciata un'eccezione di riferimento null. Immagina di essere il compilatore e devi emettere il codice per elaborare una chiamata ad abc.Def.Ghi.Jkl(), dove abc è locale, Def e Ghi sono campi di riferimento e Jkl è un metodo. Non c'è istruzione IL che possa fare qualcosa di così complicato; devi abbatterlo. Quindi emetti il ​​codice per un programma equivalente dove tutto è molto più semplice. Si emette il frammento del programma:

temp1 = abc.Def; 
temp2 = temp1.Ghi; 
temp2.Jkl(); 

Supponiamo che temp2 sia nullo perché Ghi era nullo.Questo fatto non verrà scoperto fino a quando non viene richiamata Jkl, a questo punto la cosa che lancia l'eccezione non ha alcuna conoscenza di come inizializzato temp2. Ciò è accaduto molto tempo fa, un nanosecondo nel passato e il codice macchina non ha memoria del passato; il riferimento null non tiene una piccola nota su di esso che dice da dove proviene il null, non più di quando si dice "a = b + c", il numero 12 risultante non tiene una nota insieme a quella che dice "Io ero la somma di b e c ".

+0

Grazie per la tua opinione, Eric, è bello averti in giro per rispondere a queste domande. Il fatto che io sia in modalità di debug mi dà qualche possibilità qui? Ero abbastanza sicuro che una build ottimizzata avrebbe funzionato esattamente come hai descritto, ma ho pensato che ci fosse un'opzione per fare in modo che il debugger tenesse traccia di cose come questa (a costo di prestazioni, senza dubbio). –

+0

@JustinMorgan: sei il benvenuto. Per rispondere alla tua domanda di follow-up: il debugger in questi giorni ha molte funzionalità che non conosco! Non sono un utente esperto del debugger, abbastanza stranamente. Dato il numero di funzioni che la cosa ha in questi giorni, potrebbe esserci una modalità in cui è possibile sbirciare indietro nel tempo e chiedere "come è arrivato questo valore?" Se c'è, non so cosa sia. –

+0

Anche se il codice in esecuzione non deve e non deve tenere la nota, il jitter può certamente creare una mappatura tra l'indirizzo di esecuzione e l'istruzione IL (ipotizzando un'ottimizzazione abbastanza bassa) da cui dovrebbe essere possibile dedurre quale variabile o espressione ha causato il 'NullReferenceException '. Mantiene già mappature simili per altri scopi. Non ho idea se effettivamente crei questa particolare mappatura, ma in linea di principio dovrebbe essere in grado di farlo. – CodesInChaos

1

Se si imposta un punto di interruzione qui, si dovrebbe essere in grado di ispezionare ognuno dei valori prima che la linea viene eseguito e l'eccezione viene generata. Devi solo esaminare metodicamente ogni elemento sottoposto a consultazione finché non trovi lo Null. Visual Studio è molto bravo in questo genere di cose.

+0

Il mio problema era riuscire a rompere l'espressione lambda, o idealmente avere il debugger solo dirmi il colpevole così posso risparmiare un po 'di tempo. Il commento di Joe White ** sulla domanda mi ha detto come rompere il lambda. –

2

Una soluzione per il tuo problema specifico è la riscrittura del lambda in un multilinea che valuta singolarmente ciascuna condizione e restituisce esplicitamente. È quindi possibile tracciarlo più facilmente e trovare il riferimento null.

+1

I primi due paragrafi di questa domanda sono senza senso, ma l'ultimo è un suggerimento equo. 'Any' non è pigro. – mquander

+1

Wow. Non so cosa stavo pensando. 'Qualunque()' in un foreach? Pfffftt. L'ho corretto Grazie. – Coincoin

+0

L'ho considerato un'opzione e sarei ricaduto su di esso se non avessi trovato il codice problema. Mi stavo chiedendo se fosse possibile entrare nel lambda senza farlo (il che risulta essere possibile, vedi il commento di "** White Joe" sotto la domanda). –

1

È possibile inserire un punto di interruzione all'interno dell'espressione lambda e quando viene colpito, si dovrebbe essere in grado di passare con il mouse sopra l'espressione e vedere i loro valori correttamente.

Guardando il tuo codice, posso vedere che solo una delle tre espressioni potrebbe aver causato il NullRef- r, r.RegistrationCountry e DatesWorked.

Metti queste tre espressioni nella finestra di Controllo e chiedi al debugger di interrompere qualsiasi NullReferenceException (tramite Debug-> Eccezioni), o inserisci un punto di interruzione all'interno dell'espressione lambda, e fallo un punto condizionale sulla condizione r == null || r.RegistrationCountry == null || DatesWorked == null e la risposta dovrebbe apparire abbastanza veloce.

+0

All'epoca in cui ho postato la domanda non sapevo come ottenere un punto di interruzione all'interno di un'espressione lambda, sebbene quella particolare parte abbia avuto risposta. Il punto di interruzione condizionale e le idee NRE break-on sono entrambi buoni suggerimenti, però. +1. –

1

Normalmente non risponderei senza rispondere, ma penso che la risposta che dice che non esiste un modo generale per farlo non è corretta.

Mi sembra che si possa scrivere una funzione wrapper prendendo un albero di espressioni, scomposendo ogni sottoespressione che ha proprietà e accessorie di campo e ricostruendola con controlli nulli espliciti che generano un'eccezione informativa su ciascuno di questi accessor. Pseudocodice:

static Expression<Func<T, bool>> WithNullDebugging(Expression<Func<T, bool>> exp) 
{ 
    for each node in the expression tree 
     if node is a field, property, or method accessor 
      generate a null check for this member and an exception throw 
      substitute the checked node for this node 
     else if the node has subexpression children 
      call this method recursively on each child 
      substitute each checked subexpression for the subexpression 

    return the fixed expression tree 
} 

Io di solito non lo faccio metaprogrammazione in C#, quindi non sono sicuro, ma penso che questo è del tutto possibile; qualcuno mi fa sapere se non lo è, e rimuoverò o correggerò questa risposta.

+0

Mi piace questa idea. È come un'estensione di debug per espressioni lambda. Questo particolare problema è stato risolto, ma se potrò usare qualcosa del genere in futuro, lo terrò a mente. Grazie. –

1

Il problema immediato è che lambda esegue una complessa logica complessa in una singola istruzione, quindi non è possibile trovare dove si è verificato l'arresto anomalo.

Ma questo è solo un effetto collaterale. Il problema reale è che il tuo codice presuppone, in modo errato, che nessuno dei riferimenti sarà nullo.

Un approccio sarebbe quello di provare a isloare l'incidente e mettere una benda sul "bit che si è rotto". Ma questo non attaccherà la radice del problema: ci sono assunzioni incontrollate nel codice e hai già la prova che almeno una di esse è sbagliata. Se un altro è sbagliato, a un certo punto indefinito in futuro, il programma si bloccherà di nuovo e eseguirai nuovamente il debug e il bendaggio. Questo può andare avanti e avanti e il tuo codice verrà hackerato ogni volta.

È necessario mettere giù il debugger e pensare al codice. Tutto il codice, in un unico passaggio. "Desk-check": analizza ogni parte dell'espressione e chiediti "Può essere un po 'nulla? Cosa succederà se lo è? E se sì, come posso renderlo sicuro?"

In questo modo, sarete in grado di riscrivere l'intera espressione in un modulo che conoscete è nullo e non avrete mai bisogno di eseguire il debug in esso per capire perché è esploso.

Ad esempio, questo:

r.RegistrationCountry.IsUSAOrCandada() && 

... potrebbe provocare un dereferenziamento nullo se r == null o se r.RegistrationCountry == null.Il codice deve verificare queste possibilità. Il codice "più difensiva" sarebbe quello di controllare ogni cosa di riferimento del genere:

r != null && r.RegistrationCountry != null && r.RegistrationCountry.IsUSAOrCandada() && 

che garantisce che ogni passo sarà eseguito solo se il passo precedente è riuscita. Si noti, tuttavia, che l'elenco potrebbe non fornire mai r == null, in modo che la verifica potrebbe non essere necessaria. Oppure r.RegistrationCountry può essere una struct (un tipo non annullabile), quindi saprai che il controllo non è richiesto. Quindi puoi evitare controlli superflui ripensandoci. Ma è necessario riflettere su ogni parte del codice per mettere in discussione ed eliminare tutte le ipotesi.

3

Questo non risolverà il vostro intero problema, ma dovrebbe aiutare:

È possibile impostare un punto di interruzione all'interno del lambda - non solo nel modo consueto (cliccando nella grondaia sarà punto di interruzione la dichiarazione contenente, non l'interno della lambda). Devi mettere il cursore all'interno del lambda e premere F9 - quindi otterrai un punto di interruzione all'interno del tuo lambda.