带有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,包括newobjinitobj ;
  • 声明四个本地人而不是一个。

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

  1. AB之间是否存在任何function,语义或其他重要的运行时差异? (我们只对这里的正确性感兴趣,而不是表现)
  2. 如果它们在function上相同,那么可以暴露可观察差异的运行时条件是什么?
  3. 如果它们function等价物,那么B在做什么(总是以与A相同的结果),以及是什么触发了它的痉挛? B是否有永远不会执行的分支?
  4. 如果差异是通过== 左侧显示的内容之间的差异来解释的(这里是引用表达式与文字值的属性),您是否可以指出描述详细信息的C#规范部分。
  5. 是否有可靠的经验法则可用于在编码时预测膨胀的IL ,从而避免创建它?

奖金。 每个堆栈的相应最终JITted x86AMD64代码如何?


[编辑]
基于评论中的反馈的附加说明。 首先,提出了第三种变体,但它给出了与A相同的IL(对于DebugRelease版本)。 然而,在音乐上,新的C#似乎比A更光滑:

 static void C(ISupportInitialize x) { if ((x as ISupportInitializeNotification)?.IsInitialized ?? false) throw null; } 

这里也是每个函数的Release IL。 请注意,对于Release IL,不对称A / CB仍然很明显,因此最初的问题仍然存在。

释放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(true) 。 在情况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} 。 如果你将Nullablebool进行比较,那么参数列表应该是{Nullable a, bool b} ,对于那个参数列表, Nullable.Equals(object o)方法似乎是最好的函数,因为它只需要一个从boolobject隐式转换。

但是,如果将参数列表的顺序恢复为{bool a, Nullable b}Nullable.Equals(object o)方法不再是最佳函数,因为现在您必须从Nullable转换Nullable在第一个参数中bool ,然后在第二个参数中从boolobject 。 这就是为什么情况A选择了不同的重载,这似乎导致更清晰的IL代码。

这又是一个解释,满足了我自己的好奇心, 似乎符合c#规范。 但我还没弄清楚如何调试编译器以查看实际发生的情况。