带有 Nullable<T> 的 '==' 的参数顺序

Argument order for '==' with Nullable<T>

以下两个 C# 函数的不同之处仅在于将参数的 left/right 顺序交换为 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就是:

IL 函数 '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 函数 'B'…

[0] bool flag,
[1] bool flag2,
[2] valuetype [mscorlib]Nullable`1<bool> nullable,
[3] valuetype [mscorlib]Nullable`1<bool> 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<bool>
        ldloc.3
        br.s L_0022
L_0018: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0022: stloc.2
        ldloc.1
        ldloca.s nullable
        call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
        beq.s L_0030
        ldc.i4.0
        br.s L_0037
L_0030: ldloca.s nullable
        call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0037: stloc.0
        ldloc.0
        brfalse.s L_003d
        ldnull
        throw
L_003d: ret

问题

  1. AB 之间是否存在任何功能、语义或其他实质性运行时差异? (这里我们只关心正确性,不关心性能)
  2. 如果它们在功能上相同,那么可以暴露可观察差异的运行时条件是什么?
  3. 如果它们 功能等价物,那么 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。请注意 A/CB 的不对称性在 Release 中仍然很明显IL,所以原来的问题仍然存在。

发布函数 'A'、'C'…

的 IL
        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

释放函数 'B'…

的 IL
[0] valuetype [mscorlib]Nullable`1<bool> nullable,
[1] valuetype [mscorlib]Nullable`1<bool> nullable2
        ldc.i4.1
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_0016
        pop
        ldloca.s nullable2
        initobj [mscorlib]Nullable`1<bool>
        ldloc.1
        br.s L_0020
L_0016: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0020: stloc.0
        ldloca.s nullable
        call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
        beq.s L_002d
        ldc.i4.0
        br.s L_0034
L_002d: ldloca.s nullable
        call instance bool [mscorlib]Nullable`1<bool>::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;
}

释放函数 'D'…

的 IL
[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<bool>(true)。在情况 A 中,要将某些内容与 true/false 进行比较,可以使用一条 IL 指令 (brfalse.s) 来完成。

我在C# 5.0 spec中找不到具体的参考资料。 7.10 关系和类型测试运算符 参考7.3.4 二元运算符重载解析反过来指的是7.5.3重载解析,但是后面那个很模糊

所以我很好奇答案并查看了 c# 6 规范(不知道托管 c# 7 规范的位置)。完全免责声明:我不保证我的答案是正确的,因为我没有写 c# spec/compiler 并且我对内部的理解是有限的。

但我认为答案在于 overloadable == operator. The best applicable overload for == is determined by using the rules for better function members 的结果。

来自规范:

Given an argument list A with a set of argument expressions {E1, E2, ..., En} and two applicable function members Mp and Mq with parameter types {P1, P2, ..., Pn} and {Q1, Q2, ..., Qn}, Mp is defined to be a better function member than Mq if

for each argument, the implicit conversion from Ex to Qx is not better than the implicit conversion from Ex to Px, and for at least one argument, the conversion from Ex to Px is better than the conversion from Ex to Qx.

引起我注意的是参数列表{E1, E2, .., En}。如果将 Nullable<bool>bool 进行比较,参数列表应该类似于 {Nullable<bool> a, bool b},对于该参数列表,Nullable<bool>.Equals(object o) 方法似乎是最好的函数,因为它只进行一次从 boolobject.

的隐式转换

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

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