【发布时间】: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 代码);
- 调用其他函数,包括
newobj和initobj; - 声明四个本地人而不是一个。
函数'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
问题
- A 和 B 之间是否存在任何功能、语义或其他实质性运行时差异? (我们在这里只关心正确性,而不是性能)
- 如果它们在功能上不等效,那么哪些运行时条件可以暴露出可观察到的差异?
- 如果它们是功能等价物,B 正在做什么(总是以与 A 相同的结果结束),以及触发了什么它的痉挛? B 是否有永远无法执行的分支?
- 如果差异是由出现在
==的 左侧 侧的内容之间的差异来解释的(此处为属性引用表达式与文字值),您能否指出描述细节的 C# 规范。 - 是否有可靠的经验法则可用于在编码时预测臃肿的 IL,从而避免创建它?
奖金。每个堆栈各自的最终 JITted x86 或 AMD64 代码如何?
[编辑]
基于 cmets 反馈的附加说明。首先,提出了第三种变体,但它提供与 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 的不对称性仍然很明显,因此原始问题仍然存在。 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 && y.IsInitialized)。它们都导致 IL 等于或小于 A。
标签: c# nullable equals-operator commutativity