2013-10-14 69 views
15

我發現了意想不到的NullReferenceException當運行此代碼,省略fileSystemHelper參數(因此默認爲null):爲什麼在這種情況下空合併運算符(?)不起作用?

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."); 
      } 
     } 

當我單一步驟的代碼在調試器,後餘步驟在然後fileSystem第一線(與??運營商)仍具有null值,如本屏幕剪斷(步過的下一行拋出NullReferenceException): When is null not null?

這不是我所期待的!我期待空合併運算符發現該參數爲空,並創建一個new FileSystemHelper()。我已經盯着這段代碼很長時間了,看不出它有什麼問題。

ReSharper指出該字段僅用於這一個方法,所以可能會被轉換爲局部變量...所以我試過了,猜猜是什麼?有效。所以,我有我的修復,但我不能爲我的生活看到爲什麼上面的代碼不起作用。我覺得我處於學習C#有趣的事情的邊緣,無論是我還是做了一些非常愚蠢的事情。任何人都可以看到這裏發生了什麼?

+0

您已經在方法參數中聲明'fileSystemHelper'爲'null',我不確定,但它可能與它有關。但是,我再次猜測。 – Tico

+2

您確定NRE不會出現在* GetFullPath中*(忽略手錶顯示的內容)?我沒有看到會導致上述行爲的上述代碼。 – user2864740

+0

好吧,退出VisualStudio做其他事情然後重新加載它,現在一切正常,我不能再現問題。我認爲這可能是ReSharper單元測試運行器存在的一個奇怪的緩存問題。我正在使用簡單的MSpec測試來執行代碼。在運行單元測試時,ReSharper會獲取程序集的影子副本,有時候,影子副本有時會「卡住」,我之前已經看到它發生過幾次。所以很可能,我實際上正在運行舊代碼,儘管我已經手動重建了所有內容。這是我最好的解釋... –

回答

12

我在VS2012用下面的代碼複製它:

public void Test() 
{ 
    TestFoo(); 
} 

private Foo _foo; 

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

public class Foo 
{ 
} 

如果設置在TestFoo方法的最後一個斷點,你希望看到的_foo變量集,但它仍然會顯示在調試器中爲null。

但是,如果你做什麼_foo,它然後顯示正確。即使是一個簡單的任務,如

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

如果你通過它一步,你會看到_foo顯示爲空,直到它被分配到f

這讓我想起延遲執行行爲,比如LINQ,但是我找不到任何可以證實這一點的東西。

這完全是可能的,這只是調試器的一個怪癖。也許有MSIL技能的人可以揭示一下發生了什麼。

同樣有趣的是,如果你更換空合併運算符與它的等效:

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

然後,它不會出現此行爲。

我不是一個組裝/ MSIL的傢伙,但只是考慮看看兩個版本之間的dissasembly輸出有趣的是:

 _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 

比較,爲內聯,如果版本:

 _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 

基於此,我確實認爲有某種延遲執行發生。第二個例子中的賦值語句與第一個例子相比非常小。

+3

這似乎是64位調試器的問題。構建和編譯目標「任何CPU」都表現出這種行爲。將其更改爲目標x86,則不再是問題。 –

+3

我想我知道問題是什麼......在64位版本中由VS生成的行號不正確。爲空行合併行之後的行生成的行號實際上是空合併指令的「中間」。所以調試器在那條線上斷開,但之前的指令尚未完全完成。通過該行將完成該指令。如果逐步完成反彙編,您可以看到成員變量的設置點。 –

+0

@JeffMercado - 確認。它只用「任何CPU」或「x64」執行此操作。適用於「x86」。 –

1

其他人在this question中遇到同樣的問題。有趣的是,它也使用this._field = expression ?? new ClassName();格式。這可能是調試器的某種問題,因爲寫出該值似乎爲它們產生了正確的結果。

嘗試添加調試/日誌代碼以顯示賦值後的字段值,以消除連接的調試器中的奇怪現象。

相關問題