2013-10-14 15 views
15

sto ricevendo un inaspettato NullReferenceException quando si esegue questo codice, omettendo il parametro fileSystemHelper (e quindi inadempiente a NULL):Perché l'operatore coalescente null (??) non funziona in questa situazione?

public class GitLog 
    { 
    FileSystemHelper fileSystem; 

    /// <summary> 
    /// Initializes a new instance of the <see cref="GitLog" /> class. 
    /// </summary> 
    /// <param name="pathToWorkingCopy">The path to a Git working copy.</param> 
    /// <param name="fileSystemHelper">A helper class that provides file system services (optional).</param> 
    /// <exception cref="ArgumentException">Thrown if the path is invalid.</exception> 
    /// <exception cref="InvalidOperationException">Thrown if there is no Git repository at the specified path.</exception> 
    public GitLog(string pathToWorkingCopy, FileSystemHelper fileSystemHelper = null) 
     { 
     this.fileSystem = fileSystemHelper ?? new FileSystemHelper(); 
     string fullPath = fileSystem.GetFullPath(pathToWorkingCopy); // ArgumentException if path invalid. 
     if (!fileSystem.DirectoryExists(fullPath)) 
      throw new ArgumentException("The specified working copy directory does not exist."); 
     GitWorkingCopyPath = pathToWorkingCopy; 
     string git = fileSystem.PathCombine(fullPath, ".git"); 
     if (!fileSystem.DirectoryExists(git)) 
      { 
      throw new InvalidOperationException(
       "There does not appear to be a Git repository at the specified location."); 
      } 
     } 

Quando ho solo passo il codice nel debugger, dopo faccio un passo sopra la prima linea (con l'operatore ??) allora fileSystem ha ancora il valore nullo, come mostrato in questa schermata snip (scavalcando la riga successiva tiri NullReferenceException): When is null not null?

questo non è quello che mi aspettavo! Mi aspetto che l'operatore null coalescente identifichi che il parametro è nullo e crea un new FileSystemHelper(). Ho osservato questo codice per anni e non riesco a vedere cosa c'è di sbagliato in esso.

ReSharper ha sottolineato che il campo è utilizzato solo in questo metodo, quindi potrebbe potenzialmente essere convertito in una variabile locale ... quindi l'ho provato e indovina cosa? Ha funzionato. Quindi, ho la mia correzione, ma non posso per la vita di me vedere perché il codice sopra non funziona. Mi sento come se fossi al limite di imparare qualcosa di interessante su C#, o quello o ho fatto qualcosa di veramente stupido. Qualcuno può vedere cosa sta succedendo qui?

+0

Si sta già affermando che 'fileSystemHelper' è' null' nei parametri del metodo, non sono sicuro, ma potrebbe avere qualcosa a che fare con esso. Ma poi di nuovo, sto indovinando. – Tico

+2

Sei sicuro che la NRE non si verifica * all'interno * di GetFullPath (ignorando ciò che mostra l'orologio)? Non vedo nulla con il codice di cui sopra che comporterebbe detto comportamento. – user2864740

+0

OK, dopo essere uscito da VisualStudio per fare qualcos'altro, l'ho ricaricato, tutto funziona ora e non riesco a riprodurre il problema. Credo che questo potrebbe essere un problema di memorizzazione nella cache con il runner di prova dell'unità ReSharper. Sto usando un semplice test MSpec per esercitare il codice. ReSharper esegue una copia shadow dell'assembly durante l'esecuzione dei test delle unità e talvolta, a volte, la copia shadow sembra bloccata, l'ho vista accadere un paio di volte prima. Quindi, molto probabilmente, stavo effettivamente eseguendo il vecchio codice, anche se avevo ricostruito manualmente tutto. È la migliore spiegazione che ho ... –

risposta

12

ho riprodotto in VS2012 con il seguente codice:

public void Test() 
{ 
    TestFoo(); 
} 

private Foo _foo; 

private void TestFoo(Foo foo = null) 
{ 
    _foo = foo ?? new Foo(); 
} 

public class Foo 
{ 
} 

Se si imposta un punto di interruzione alla fine del metodo TestFoo, ci si aspetterebbe di vedere la variabile stabilita _foo, ma mostrerà ancora come null nel debugger.

Ma se si fa quindi qualcosa con _foo, allora viene visualizzato correttamente. Anche un incarico semplice, come

_foo = foo ?? new Foo(); 
var f = _foo; 

Se fate un passo attraverso di essa, vedrete che _foo mostra nulla fino a quando non viene assegnato a f.

Questo mi ricorda il comportamento di esecuzione differita, come con LINQ, ma non riesco a trovare nulla che possa confermarlo.

È del tutto possibile che questa sia solo una stranezza del debugger. Forse qualcuno con competenze MSIL può far luce su ciò che sta accadendo sotto il cofano.

anche interessante è che se si sostituisce l'operatore null coalescenza con il suo equivalente:

_foo = foo != null ? foo : new Foo(); 

Allora non presenta questo comportamento.

Io non sono un assemblaggio/MSIL ragazzo, ma basta dare un'occhiata a l'uscita dissasembly tra le due versioni è interessante:

 _foo = foo ?? new Foo(); 
0000002d mov   rax,qword ptr [rsp+68h] 
00000032 mov   qword ptr [rsp+28h],rax 
00000037 mov   rax,qword ptr [rsp+60h] 
0000003c mov   qword ptr [rsp+30h],rax 
00000041 cmp   qword ptr [rsp+68h],0 
00000047 jne   0000000000000078 
00000049 lea   rcx,[FFFE23B8h] 
00000050 call  000000005F2E8220 
     var f = _foo; 
00000055 mov   qword ptr [rsp+38h],rax 
0000005a mov   rax,qword ptr [rsp+38h] 
0000005f mov   qword ptr [rsp+40h],rax 
00000064 mov   rcx,qword ptr [rsp+40h] 
00000069 call  FFFFFFFFFFFCA000 
0000006e mov   r11,qword ptr [rsp+40h] 
00000073 mov   qword ptr [rsp+28h],r11 
00000078 mov   rcx,qword ptr [rsp+30h] 
0000007d add   rcx,8 
00000081 mov   rdx,qword ptr [rsp+28h] 
00000086 call  000000005F2E72A0 
0000008b mov   rax,qword ptr [rsp+60h] 
00000090 mov   rax,qword ptr [rax+8] 
00000094 mov   qword ptr [rsp+20h],rax 

Confronta che al inline-se la versione:

 _foo = foo != null ? foo : new Foo(); 
0000002d mov   rax,qword ptr [rsp+50h] 
00000032 mov   qword ptr [rsp+28h],rax 
00000037 cmp   qword ptr [rsp+58h],0 
0000003d jne   0000000000000066 
0000003f lea   rcx,[FFFE23B8h] 
00000046 call  000000005F2E8220 
0000004b mov   qword ptr [rsp+30h],rax 
00000050 mov   rax,qword ptr [rsp+30h] 
00000055 mov   qword ptr [rsp+38h],rax 
0000005a mov   rcx,qword ptr [rsp+38h] 
0000005f call  FFFFFFFFFFFCA000 
00000064 jmp   0000000000000070 
00000066 mov   rax,qword ptr [rsp+58h] 
0000006b mov   qword ptr [rsp+38h],rax 
00000070 nop 
00000071 mov   rcx,qword ptr [rsp+28h] 
00000076 add   rcx,8 
0000007a mov   rdx,qword ptr [rsp+38h] 
0000007f call  000000005F2E72A0 
     var f = _foo; 
00000084 mov   rax,qword ptr [rsp+50h] 
00000089 mov   rax,qword ptr [rax+8] 
0000008d mov   qword ptr [rsp+20h],rax 

Sulla base di questo, penso che ci sia una specie di esecuzione differita che si sta verificando. L'istruzione di assegnazione nel secondo esempio è molto piccola rispetto al primo esempio.

+3

Questo sembra essere un problema con il debugger a 64 bit. Costruire e compilare il targeting "Qualsiasi CPU" mostra questo comportamento. Cambiandolo per target x86, quindi non è più un problema. –

+3

Penso di sapere qual è il problema ... i numeri di riga generati da VS nella build a 64 bit non sono corretti. Il numero di linea generato per la linea che segue la linea a coalescenza nulla è in realtà "al centro" dell'istruzione a coalescenza nulla. Quindi il debugger si interrompe su quella linea ma l'istruzione precedente non è stata ancora completamente completata. Superando quella linea finirai quella istruzione. Puoi vedere il punto in cui la variabile membro viene impostata se passi attraverso il disassemblaggio. –

+0

@JeffMercado - Confermato. Lo fa solo con "Any CPU" o "x64". Funziona bene con "x86". –

1

Qualcun altro ha riscontrato lo stesso problema in this question. È interessante notare che sta anche utilizzando il formato this._field = expression ?? new ClassName();. Potrebbe essere una sorta di problema con il debugger, in quanto la scrittura del valore sembrava produrre i risultati corretti per loro.

Provare ad aggiungere il codice di debug/log per mostrare il valore del campo dopo l'assegnazione per eliminare le stranezze nel debugger collegato.

Problemi correlati