这与空条件运算符无关。您可以通过将其替换为普通成员访问来轻松看到这一点:
Console.WriteLine(t == null); //outputs false
Console.WriteLine(t.Name == null); //outputs false
对新XYZClass
对象的原始引用在调试构建(并在调试器下运行)中永远不会“超出范围”。在 LINQPad 中关闭优化,您还会看到它t
不为空。但请注意,所有这些都是实现细节 - 根据您系统的具体情况,您可以获得任一结果(例如,我得到了您在 32 位调试版本上得到的结果,但不是 64 位调试版本)。
.NET 中托管对象生命周期的唯一保证是终结器外部的强引用将阻止对象被收集。忘记所有确定性内存管理——它根本不存在。完全没有垃圾收集器的 .NET 实现将是完全有效的。
因此,让我们特别看一下在我的机器上生成的代码。在 64 位构建中,t.Name == null
得到t?.Name == null
完全相同的结果(当然t.Name == null
会导致 aNullReferenceException
而不是返回 true)。那么 32 位版本呢?
该t.Name == null
部分大大缩短:
00533111 mov ecx,dword ptr [ebp-44h] ; t
00533114 cmp dword ptr [ecx],ecx ; null check
00533116 call 00530D28 ; t.get_Name
0053311B mov dword ptr [ebp-54h],eax ; Name string
0053311E cmp dword ptr [ebp-54h],0 ; is null?
00533122 sete cl
00533125 movzx ecx,cl
00533128 call 708B09F4
您可以看到我们使用了两个寄存器(ecx 和 eax),以及两个堆栈槽(-44h 和 -54h)。那个t?.Name == null
呢?
001F3111 cmp dword ptr [ebp-44h],0 ; is t null?
001F3115 jne 001F311F
001F3117 nop
001F3118 xor edx,edx
001F311A mov dword ptr [ebp-54h],edx ; result is false
001F311D jmp 001F312A
001F311F mov ecx,dword ptr [ebp-44h] ; t
001F3122 call 001F0D28 ; t.get_Name
001F3127 mov dword ptr [ebp-54h],eax
001F312A cmp dword ptr [ebp-54h],0 ; is name null?
001F312E sete cl
001F3131 movzx ecx,cl
001F3134 call 708B09F4
001F3139 nop
我们仍然使用相同的两个堆栈槽,但需要另一个寄存器 - edx。这可能是我们正在寻找的吗?完全正确!如果我们看一下对象最初是如何创建的:
001F30A0 mov ecx,2C0814h
001F30A5 call 001330F4 ; new XYZClass
001F30AA mov dword ptr [ebp-48h],eax ; tmp
001F30AD mov ecx,dword ptr [ebp-48h]
001F30B0 call 001F0D38 ; tmp.XYZClass()
001F30B5 mov edx,dword ptr ds:[36B230Ch] ; "bleh"
001F30BB mov ecx,dword ptr [ebp-48h]
001F30BE cmp dword ptr [ecx],ecx
001F30C0 call 001F0D30 ; tmp.set_Name("bleh")
001F30C5 nop
001F30C6 mov ecx,2C0858h
001F30CB call 710F9ECF ; new WeakReference
001F30D0 mov dword ptr [ebp-4Ch],eax
001F30D3 mov ecx,dword ptr [ebp-4Ch]
001F30D6 mov edx,dword ptr [ebp-48h] ; EDX references tmp!
001F30D9 call 709090B0
001F30DE mov eax,dword ptr [ebp-4Ch]
001F30E1 mov dword ptr [ebp-40h],eax
您可以看到,空条件版本使用的寄存器与用于保存对 的临时引用的寄存器相同XYZClass
。这就是差异的根源——运行时不能排除edx
访问是对临时引用的使用,因此它会安全地播放它并保持对象的根,从而防止它被收集。
64 位版本(并且在没有附加调试器的情况下运行)没有看到差异,因为它重用了不同的寄存器 - 在我的特定机器上,64 位版本重用rcx
(包含对WeakReference
, not的引用XYZClass
),并且非调试器 32 位版本重用eax
(包含对 的引用"bleh"
)。由于edx
(and rdx
) 从未在该方法中使用,因此临时引用不再是根目录,并且可以自由收集。
为什么调试器版本edx
特别使用?最有可能的是,它试图提供帮助。在 null 条件运算符的中间,您希望同时查看t
and的值t?.Name
,以便更好地访问它们(您可以在 Locals 中将其视为“XYZClass.Name.get 返回“bleh”字符串”)。
同样,请注意,这完全是特定于实现的。合同仅指定何时不得回收对象 - 它没有说明何时回收。