带有 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就是:
- 长 36 个字节(IL 代码);
- 调用其他函数,包括
newobj
和 initobj
;
- 声明四个本地人而不是一个本地人。
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
问题
- A 和 B 之间是否存在任何功能、语义或其他实质性运行时差异? (这里我们只关心正确性,不关心性能)
- 如果它们在功能上不相同,那么可以暴露可观察差异的运行时条件是什么?
- 如果它们 是 功能等价物,那么 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。请注意 A/C 与 B 的不对称性在 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)
方法似乎是最好的函数,因为它只进行一次从 bool
到 object
.
的隐式转换
但是,如果您将参数列表的顺序恢复为 {bool a, Nullable<bool> b}
,则 Nullable<bool>.Equals(object o)
方法不再是最佳函数,因为现在您必须将 Nullable<bool>
转换为 bool
在第一个参数中,然后在第二个参数中从 bool
到 object
。这就是为什么对于 case A 选择了不同的重载,这似乎导致了更清晰的 IL 代码。
同样,这是满足我自己好奇心的解释,似乎符合 c# 规范。但我还没有弄清楚如何调试编译器以查看实际发生的情况。
以下两个 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就是:
- 长 36 个字节(IL 代码);
- 调用其他函数,包括
newobj
和initobj
; - 声明四个本地人而不是一个本地人。
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
问题
- A 和 B 之间是否存在任何功能、语义或其他实质性运行时差异? (这里我们只关心正确性,不关心性能)
- 如果它们在功能上不相同,那么可以暴露可观察差异的运行时条件是什么?
- 如果它们 是 功能等价物,那么 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。请注意 A/C 与 B 的不对称性在 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)
方法似乎是最好的函数,因为它只进行一次从 bool
到 object
.
但是,如果您将参数列表的顺序恢复为 {bool a, Nullable<bool> b}
,则 Nullable<bool>.Equals(object o)
方法不再是最佳函数,因为现在您必须将 Nullable<bool>
转换为 bool
在第一个参数中,然后在第二个参数中从 bool
到 object
。这就是为什么对于 case A 选择了不同的重载,这似乎导致了更清晰的 IL 代码。
同样,这是满足我自己好奇心的解释,似乎符合 c# 规范。但我还没有弄清楚如何调试编译器以查看实际发生的情况。