环境是.net Framework 3.5
考虑到这样一组代码:
Type a = new Type();
Type b = new Type();
a = a ?? b;
和
Type a = new Type();
Type b = new Type();
a = (null == a ? b : a);
在正常情况下, 二者的结果是一样的, 可是谁的效率更高呢?
设计如下的代码实验:
code snippet A:
{
int? a;
int? b = 3;
a = b;
a = a ?? b;
}
其il代码如下
{
.entrypoint
// Code size 40 (0x28)
.maxstack 3
.locals init ([0] valuetype [mscorlib]System.Nullable`1<int32> a,
[1] valuetype [mscorlib]System.Nullable`1<int32> b,
[2] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000)
IL_0000: nop
IL_0001: ldloca.s b
IL_0003: ldc.i4.3
IL_0004: call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0009: nop
IL_000a: ldloc.1
IL_000b: stloc.0
IL_000c: ldloc.0
IL_000d: stloc.2
IL_000e: ldloca.s CS$0$0000
IL_0010: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_0015: brtrue.s IL_001a
IL_0017: ldloc.1
IL_0018: br.s IL_0026
IL_001a: ldloca.s CS$0$0000
IL_001c: call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_0021: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0026: stloc.0
IL_0027: ret
}
而code snippet B:
{
int? a;
int? b = 3;
a = b;
a = (null == a ? b : a);
}
其il代码如下
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 2
.locals init ([0] valuetype [mscorlib]System.Nullable`1<int32> a,
[1] valuetype [mscorlib]System.Nullable`1<int32> b)
IL_0000: nop
IL_0001: ldloca.s b
IL_0003: ldc.i4.3
IL_0004: call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0009: nop
IL_000a: ldloc.1
IL_000b: stloc.0
IL_000c: ldloca.s a
IL_000e: call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_0013: brfalse.s IL_0018
IL_0015: ldloc.0
IL_0016: br.s IL_0019
IL_0018: ldloc.1
IL_0019: stloc.0
IL_001a: ret
}
两者在000b也就是 a = b 之前都是相同的, 包括二者的判断代码也类似, 虽然一个走的是判断是否为真的路子而一个走的是是否为假, 这都正常. 不同点出现在判断之前和之后的赋值上. 其实很明显可以看出来snippet1的il里面都还存了一个loc.2, 索引为CS$0$0000. 但是a = a ?? b所作的是将a存在loc.2里, 由索引CS$0$0000进行是否为空的判断, 而CS$0$0000默认就是指向a的. 也就是snippet1不像是snippet2里那样做的, 直接load索引a对其进行是否为空的判断, 而是把它当成一个临时函数, 存入一个临时变量也就是CS$0$0000, null coalescing的开销要大于ternary conditional. 而且就算是DateTime?这样的复杂类型也是这样, 奇怪吧.
由于int? aka Nullable<int>是值类型, 我们再看一个引用类型的例子.
我们可以写一个空的class A, 考虑以下代码
考虑到这样一组代码:
A a = new A();
A b = new A();
a = a ?? b;
with il
{
.entrypoint
// Code size 23 (0x17)
.maxstack 2
.locals init ([0] class ExperimentalOne.Program/A a,
[1] class ExperimentalOne.Program/A b)
IL_0000: nop
IL_0001: newobj instance void ExperimentalOne.Program/A::.ctor()
IL_0006: stloc.0
IL_0007: newobj instance void ExperimentalOne.Program/A::.ctor()
IL_000c: stloc.1
IL_000d: ldloc.1
IL_000e: stloc.0
IL_000f: ldloc.0
IL_0010: dup
IL_0011: brtrue.s IL_0015
IL_0013: pop
IL_0014: ldloc.1
IL_0015: stloc.0
IL_0016: ret
}
和
A a = new A();
A b = new A();
a = (null == a ? b : a);
with il
{
.entrypoint
// Code size 24 (0x18)
.maxstack 2
.locals init ([0] class ExperimentalOne.Program/A a,
[1] class ExperimentalOne.Program/A b)
IL_0000: nop
IL_0001: newobj instance void ExperimentalOne.Program/A::.ctor()
IL_0006: stloc.0
IL_0007: newobj instance void ExperimentalOne.Program/A::.ctor()
IL_000c: stloc.1
IL_000d: ldloc.1
IL_000e: stloc.0
IL_000f: ldloc.0
IL_0010: brfalse.s IL_0015
IL_0012: ldloc.0
IL_0013: br.s IL_0016
IL_0015: ldloc.1
IL_0016: stloc.0
IL_0017: ret
}
在这里, 代码却被做了优化, 由于少做了一次判断, ??怎么也比?:快, 如果用string或者复杂类型的class做对比也是这样.
我不知道编译器究竟是怎么想的才把机制设定成这样, 就算我加上ref关键字也同样表现. 我想以后我在写Nullable的值类型设计是否为空判断的时候会用?:, 而引用类型则是??了, 当然如果team doc里要求代码一致性不能这么写的话那就另当别论了-_-