带有Nullable 的’==’的参数顺序
以下两个C#
函数的区别仅在于将参数的左/右顺序交换为equals运算符==
。 ( IsInitialized
的类型是bool
)。 使用C#7.1和.NET 4.7 。
static void A(ISupportInitialize x) { if ((x as ISupportInitializeNotification)?.IsInitialized == true) throw null; }
static void B(ISupportInitialize x) { if (true == (x as ISupportInitializeNotification)?.IsInitialized) throw null; }
但是第二个的IL代码似乎要复杂得多。 例如, B是:
- 36个字节(IL代码);
- 调用其他function,包括
newobj
和initobj
; - 声明四个本地人而不是一个。
IL用于function’A’……
[0] bool flag nop ldarg.0 isinst [System]ISupportInitializeNotification dup brtrue.s L_000e pop ldc.i4.0 br.s L_0013 L_000e: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized() L_0013: stloc.0 ldloc.0 brfalse.s L_0019 ldnull throw L_0019: ret
IL用于function’B’……
[0] bool flag, [1] bool flag2, [2] valuetype [mscorlib]Nullable`1 nullable, [3] valuetype [mscorlib]Nullable`1 nullable2 nop ldc.i4.1 stloc.1 ldarg.0 isinst [System]ISupportInitializeNotification dup brtrue.s L_0018 pop ldloca.s nullable2 initobj [mscorlib]Nullable`1 ldloc.3 br.s L_0022 L_0018: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized() newobj instance void [mscorlib]Nullable`1::.ctor(!0) L_0022: stloc.2 ldloc.1 ldloca.s nullable call instance !0 [mscorlib]Nullable`1::GetValueOrDefault() beq.s L_0030 ldc.i4.0 br.s L_0037 L_0030: ldloca.s nullable call instance bool [mscorlib]Nullable`1::get_HasValue() L_0037: stloc.0 ldloc.0 brfalse.s L_003d ldnull throw L_003d: ret
Quesions
- A和B之间是否存在任何function,语义或其他重要的运行时差异? (我们只对这里的正确性感兴趣,而不是表现)
- 如果它们在function上不相同,那么可以暴露可观察差异的运行时条件是什么?
- 如果它们是function等价物,那么B在做什么(总是以与A相同的结果),以及是什么触发了它的痉挛? B是否有永远不会执行的分支?
- 如果差异是通过
==
左侧显示的内容之间的差异来解释的(这里是引用表达式与文字值的属性),您是否可以指出描述详细信息的C#规范部分。 - 是否有可靠的经验法则可用于在编码时预测膨胀的IL ,从而避免创建它?
奖金。 每个堆栈的相应最终JITted x86
或AMD64
代码如何?
[编辑]
基于评论中的反馈的附加说明。 首先,提出了第三种变体,但它给出了与A相同的IL(对于Debug
和Release
版本)。 然而,在音乐上,新的C#似乎比A更光滑:
static void C(ISupportInitialize x) { if ((x as ISupportInitializeNotification)?.IsInitialized ?? false) throw null; }
这里也是每个函数的Release
IL。 请注意,对于Release
IL,不对称A / C与B仍然很明显,因此最初的问题仍然存在。
释放IL用于function’A’,’C’……
ldarg.0 isinst [System]ISupportInitializeNotification dup brtrue.s L_000d pop ldc.i4.0 br.s L_0012 L_000d: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized() brfalse.s L_0016 ldnull throw L_0016: ret
释放IL用于function’B’……
[0] valuetype [mscorlib]Nullable`1 nullable, [1] valuetype [mscorlib]Nullable`1 nullable2 ldc.i4.1 ldarg.0 isinst [System]ISupportInitializeNotification dup brtrue.s L_0016 pop ldloca.s nullable2 initobj [mscorlib]Nullable`1 ldloc.1 br.s L_0020 L_0016: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized() newobj instance void [mscorlib]Nullable`1::.ctor(!0) L_0020: stloc.0 ldloca.s nullable call instance !0 [mscorlib]Nullable`1::GetValueOrDefault() beq.s L_002d ldc.i4.0 br.s L_0034 L_002d: ldloca.s nullable call instance bool [mscorlib]Nullable`1::get_HasValue() L_0034: brfalse.s L_0038 ldnull throw L_0038: ret
最后,提到了一个使用新C#7语法的版本,它似乎产生了最干净的IL:
static void D(ISupportInitialize x) { if (x is ISupportInitializeNotification y && y.IsInitialized) throw null; }
释放IL用于function’D’……
[0] class [System]ISupportInitializeNotification y ldarg.0 isinst [System]ISupportInitializeNotification dup stloc.0 brfalse.s L_0014 ldloc.0 callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized() brfalse.s L_0014 ldnull throw L_0014: ret
看起来第一个操作数被转换为第二个类型以进行比较。
情况B中的多余操作涉及构造Nullable
。 在情况A中,为了将某些东西与true
/ false
进行比较,有一条IL指令( brfalse.s
)可以做到这一点。
我在C#5.0规范中找不到具体的参考。 7.10关系型和类型测试运算符是指7.3.4二进制运算符重载 决策 ,后者又指7.5.3过载分辨率 ,但后者非常模糊。
所以我对答案感到好奇,并看了一下c#6规范(没有提供c#7规范托管的线索)。 完全免责声明:我不保证我的答案是正确的,因为我没有编写c#规范/编译器,而且我对内部的理解是有限的。
但我认为答案在于可重载 ==
运算符的结果。 ==
的最佳适用重载是通过使用更好的函数成员的规则来确定的。
从规格:
给定一个参数列表A,其中包含一组参数表达式{E1,E2,…,En}和两个适用的函数成员Mp和Mq,参数类型为{P1,P2,…,Pn}和{Q1,Q2, …,Qn},Mp被定义为比Mq更好的函数成员if
对于每个参数,从Ex到Qx的隐式转换并不比从Ex到Px的隐式转换更好,并且对于至少一个参数,从Ex到Px的转换优于从Ex到Qx的转换。
引起我注意的是参数列表{E1, E2, .., En}
。 如果你将Nullable
与bool
进行比较,那么参数列表应该是{Nullable
,对于那个参数列表, Nullable
方法似乎是最好的函数,因为它只需要一个从bool
到object
隐式转换。
但是,如果将参数列表的顺序恢复为{bool a, Nullable
则Nullable
方法不再是最佳函数,因为现在您必须从Nullable
转换Nullable
在第一个参数中bool
,然后在第二个参数中从bool
到object
。 这就是为什么情况A选择了不同的重载,这似乎导致更清晰的IL代码。
这又是一个解释,满足了我自己的好奇心, 似乎符合c#规范。 但我还没弄清楚如何调试编译器以查看实际发生的情况。