【问题标题】:Argument order for '==' with Nullable<T>带有 Nullable<T> 的 '==' 的参数顺序
【发布时间】:2018-01-26 20:42:07
【问题描述】:

以下两个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 代码);
  • 调用其他函数,包括newobjinitobj
  • 声明四个本地人而不是一个。

函数'A'的IL…

[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

函数'B'的IL…

[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 代码如何?


[编辑]

基于 cmets 反馈的附加说明。首先,提出了第三种变体,但它提供与 A 相同的 IL(对于 DebugRelease 构建)。然而,从语法上讲,新版本的 C# 似乎比 A 更时尚:

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

这里也是每个函数的Release IL。请注意,Release IL 中 A/CB 的不对称性仍然很明显,因此原始问题仍然存在。 p>

为函数 '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

【问题讨论】:

  • 在第一种情况下,当编译器看到 left 一侧的第一个调用为空时,编译器能够短路整个检查,这意味着左侧参数是null 不管它包含什么。将其与 null 以外的任何内容进行比较都保证为假。
  • 在第二种情况下,虽然左边部分为真,所以不涉及短路。编译器必须计算整个表达式的结果。如果您检查调用,由于使用?.,它正在创建一个可为空的对象,并且如果可为空有一个值,则尝试调用 IsInitialized
  • 你能在你的例子中去掉? 运算符并再次检查,它会给出相同的结果吗?
  • 这是在关闭优化的情况下生成的代码。你应该期望它没有被优化。
  • 您可以使用空传播和空合并运算符的惯用组合:if ((x as ISupportInitializeNotification)?.IsInitialized ?? false)。更好的是带有模式的新 is 表达式:if (x is ISupportInitializeNotification y &amp;&amp; y.IsInitialized)。它们都导致 IL 等于或小于 A。

标签: c# nullable equals-operator commutativity


【解决方案1】:

看起来第一个操作数被转换为第二个的类型以进行比较。

案例 B 中的多余操作涉及构造 Nullable&lt;bool&gt;(true)。而在情况 A 中,要将某物与 true/false 进行比较,只有一条 IL 指令 (brfalse.s) 可以做到这一点。

我在C# 5.0 spec 中找不到具体的参考。 7.10 关系和类型测试运算符指的是 7.3.4 二元运算符重载决议,而后者又指 7.5.3 重载分辨率,但后一种很模糊。

【讨论】:

  • 很模糊?这是 C# 规范中最详细的部分。也许您的意思是神秘,因为普通的人类读者几乎不可能理解,但这完全是另一回事。
【解决方案2】:

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

但我认为答案在于 overloadable == 运算符的结果。 == 的最佳适用过载是通过使用 better function members 的规则确定的。

来自规范:

给定一个带有一组参数表达式 {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&lt;bool&gt;bool 进行比较,则参数列表应该类似于{Nullable&lt;bool&gt; a, bool b},对于该参数列表,Nullable&lt;bool&gt;.Equals(object o) 方法似乎是最好的函数,因为它只需要从bool 进行一次隐式转换到object

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

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

【讨论】:

    猜你喜欢
    • 2016-08-02
    • 2010-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-01
    • 2021-10-30
    • 1970-01-01
    相关资源
    最近更新 更多