小心!多次访问Nullable<T>.Value属性的陷阱

问题

假设有如下代码:.

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
)

// 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());
}

小心!多次访问Nullable<T>.Value属性的陷阱

MyIO

微软 MVP,专注 .NET 领域

415篇原创内容

公众号