为啥 C# 编译器将这个 != 比较翻译为 > 比较?
Posted
技术标签:
【中文标题】为啥 C# 编译器将这个 != 比较翻译为 > 比较?【英文标题】:Why does the C# compiler translate this != comparison as if it were a > comparison?为什么 C# 编译器将这个 != 比较翻译为 > 比较? 【发布时间】:2015-05-01 03:43:07 【问题描述】:我偶然发现 C# 编译器会转用这个方法:
static bool IsNotNull(object obj)
return obj != null;
…进入这个CIL:
.method private hidebysig static bool IsNotNull(object obj) cil managed
ldarg.0 // obj
ldnull
cgt.un
ret
…或者,如果您更喜欢查看反编译的 C# 代码:
static bool IsNotNull(object obj)
return obj > null; // (note: this is not a valid C# expression)
!=
怎么会被翻译成“>
”?
【问题讨论】:
【参考方案1】:简答:
IL中没有“比较不等于”指令,所以C#!=
操作符没有精确对应关系,不能直译。
但是有一个“比较相等”指令(ceq
,直接对应于 ==
运算符),所以在一般情况下,x != y
被翻译成稍长的等效 (x == y) == false
。
还有在 IL (cgt
) 中有一个“compare-greater-than”指令,它允许编译器采用某些快捷方式(即生成更短的 IL 代码),其中之一是不等式比较针对 null 的对象,obj != null
,被翻译为好像它们是“obj > null
”。
让我们更详细一点。
如果 IL 中没有“比较不等于”指令,那么编译器将如何翻译以下方法?
static bool IsNotEqual(int x, int y)
return x != y;
如上所述,编译器会将x != y
转换为(x == y) == false
:
.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed
ldarg.0 // x
ldarg.1 // y
ceq
ldc.i4.0 // false
ceq // (note: two comparisons in total)
ret
事实证明,编译器并不总是产生这种相当冗长的模式。让我们看看当我们用常量 0 替换 y
时会发生什么:
static bool IsNotZero(int x)
return x != 0;
生成的 IL 比一般情况下要短一些:
.method private hidebysig static bool IsNotZero(int32 x) cil managed
ldarg.0 // x
ldc.i4.0 // 0
cgt.un // (note: just one comparison)
ret
编译器可以利用有符号整数存储在two's complement 中这一事实(其中,如果结果位模式被解释为无符号整数——这就是.un
的意思——0 具有最小的可能值),所以它会将x == 0
翻译成unchecked((uint)x) > 0
。
事实证明编译器可以对null
进行不等式检查:
static bool IsNotNull(object obj)
return obj != null;
编译器生成的 IL 与 IsNotZero
几乎相同:
.method private hidebysig static bool IsNotNull(object obj) cil managed
ldarg.0
ldnull // (note: this is the only difference)
cgt.un
ret
显然,允许编译器假定null
引用的位模式是任何对象引用可能的最小位模式。
Common Language Infrastructure Annotated Standard (1st edition from Oct 2003)(第 491 页,作为表 6-4 “二进制比较或分支操作”的脚注)明确提到了此快捷方式:
"
cgt.un
在 ObjectRefs (O) 上是允许且可验证的。这通常在将 ObjectRef 与 null 进行比较时使用(没有“比较不等于”指令,否则会更明显解决方案)。”
【讨论】:
很好的答案,只有一点点:二进制补码在这里不相关。重要的是,有符号整数的存储方式应使int
范围内的非负值在int
中具有与uint
中相同的表示形式。这比二进制补码要弱得多。
无符号类型永远不会有任何负数,因此与零进行比较的比较操作不能将任何非零数视为小于零。 int
的非负值对应的所有表示都已经被uint
中的同一个值占用了,所以int
的负值对应的所有表示都必须对应于some uint
的值大于 0x7FFFFFFF
,但它并不重要。 (实际上,真正需要的是在int
和uint
中以相同的方式表示零。)
@hvd:感谢您的解释。你是对的,重要的不是二进制补码;这是that you mentioned 和的要求,cgt.un
将int
视为uint
而不会更改底层位模式。 (想象一下cgt.un
会首先尝试通过将所有负数映射到 0 来修复下溢。在这种情况下,您显然不能用> 0
替换!= 0
。)
我发现使用 >
将对象引用与另一个对象引用进行比较是可验证的 IL,这让我感到惊讶。这样就可以比较两个非空对象并得到一个布尔结果(这是非确定性的)。这不是内存安全问题,但感觉像是不干净的设计,不符合安全托管代码的一般精神。这种设计泄露了对象引用被实现为指针的事实。似乎是 .NET CLI 的设计缺陷。
@usr:当然! CLI standard 的第 III.1.1.4 节说 “对象引用(O 型)是完全不透明的” 并且 “唯一允许的比较操作是相等和不等......” 可能是因为对象引用没有根据内存地址来定义,所以该标准还注意在概念上将空引用与 0 分开(参见例如ldnull
、initobj
的定义和newobj
)。因此,使用cgt.un
将对象引用与空引用进行比较似乎在不止一个方面与第 III.1.1.4 节相矛盾。以上是关于为啥 C# 编译器将这个 != 比较翻译为 > 比较?的主要内容,如果未能解决你的问题,请参考以下文章
编译原理实战入门:用 JavaScript 写一个简单的四则运算编译器词法分析
为啥将class反编译为java后,java直接编译时有错误