问题
假设有如下代码:
Demo? demo = new Demo();
for (int i = 0; i < 3; i++)
{
Console.WriteLine(demo.Value.Count());
}
public struct Demo
{
private int current;
public int Count()
{
current++;
return current;
}
}
请问,在5秒内你能说出这段代码的输出结果吗?
答案
如果你的答案是:
1
2
3
其实,正确答案是:
1
1
1
那么,这是为什么呢?
解析
这是由于 demo 对象的实际类型是 Nullable<T>,在 for 循环中,每次都需要调用 demo.Value 返回 Demo 对象。而 Nullable.Value 属性的定义是:获取当前 Nullable对象的值。也就是说,当你访问该属性时,它会将值复制到堆栈上后再使用它。因此,每次调用 demo.Value 都会返回同一个 Demo 对象的新副本。这个结论可以通过上述示例的 IL 代码来印证:
.locals init (
[0] valuetype [System.Runtime]System.Nullable`1<valuetype Demo> demo,
[1] valuetype Demo,
[2] int32 i,
[3] bool
)
// 堆代码 duidaima.com
// loop start (head: IL_002f)
IL_0014: nop
IL_0015: ldloca.s 0 // 将位于索引 0 的局部变量(demo)的地址加载到堆栈上
IL_0017: call instance !0 valuetype [System.Runtime]System.Nullable`1<valuetype Demo>::get_Value() // 获得 demo.Value
IL_001c: stloc.1 // 将当前值(demo.Value)存储在索引 1 处的局部变量中(Demo 副本)
IL_001d: ldloca.s 1 // 将位于索引 1 的局部变量(Demo 副本)的地址加载到堆栈上
IL_001f: call instance int32 Demo::Count() // 调用 Demo 副本 的Count() 方法
IL_0024: call void [System.Console]System.Console::WriteLine(int32)
总结
在使用 Nullable<T> 时,应该尽量避免多次访问 Value 属性,而应该将其缓存到一个变量中。
var tmp = demo.Value;
for (int i = 0; i < 3; i++)
{
Console.WriteLine(tmp.Count());
}