C#Nullable Equality Operations,为什么null <= null解析为false?

为什么在.NET中呢?

null >= null 

解析为假,但是

 null == null 

解析为真?

换句话说,为什么不是null >= null等效于null > null || null == null null > null || null == null

有人有正式答案吗?

此行为在C#规范( ECMA-334 )的第14.2.7节中定义(我已突出显示相关部分):

对于关系运算符

 < > <= >= 

如果操作数类型都是非可空值类型并且结果类型是bool ,则存在提升forms的运算符。 提升forms是通过添加一个? 每个操作数类型的修饰符。 如果一个或两个操作数为null则提升的运算符将生成值false 。 否则,提升的运算符解包操作数并应用基础运算符以产生bool结果。

特别是,这意味着通常的关系法则不成立; x >= y并不意味着!(x < y)

血淋淋的细节

有些人问为什么编译器决定这是int?的提升运算符int? 首先。 我们来看一下。 🙂

我们从14.2.4开始,'二元运算符重载决策'。 这详细说明了要遵循的步骤。

  1. 首先,检查用户定义的运算符的适用性。 这是通过检查由>= ...每一侧的类型定义的运算符来完成的,这就提出了null类型的问题! null文字在给定之前实际上没有任何类型,它只是“null literal”。 通过遵循14.2.5下的指示,我们发现这里没有适合的运算符,因为null literal没有定义任何运算符。

  2. 此步骤指示我们检查适用性的预定义运算符集。 (本节也排除了枚举,因为任何一方都不是枚举类型。)相关的预定义运算符在14.9.1到14.9.3节中列出,它们都是原始数字类型的运算符,以及提升的版本这些运算符(注意这里不包括string s运算符)。

  3. 最后,我们必须使用这些运算符和14.4.2中的规则来执行重载决策。

实际上执行这个决议将是非常乏味的,但幸运的是有一个捷径。 在14.2.6下,有一个提供超载解决结果的信息示例,其中说明:

...考虑二元*运算符的预定义实现:

 int operator *(int x, int y); uint operator *(uint x, uint y); long operator *(long x, long y); ulong operator *(ulong x, ulong y); void operator *(long x, ulong y); void operator *(ulong x, long y); float operator *(float x, float y); double operator *(double x, double y); decimal operator *(decimal x, decimal y); 

当重载决策规则(第14.4.2节)应用于这组运算符时,效果是从操作数类型中选择存在隐式转换的第一个运算符。

由于双方都是null我们可以立即抛弃所有未提升的运营商。 这使我们在所有原始数字类型上使用了提升的数字运算符。

然后,使用先前的信息,我们选择存在隐式转换的第一个运算符。 由于null文本可以隐式转换为可空类型,并且int存在可空类型,我们从列表中选择第一个运算符,即int? >= int? int? >= int?

许多答案都符合规范。 事实certificate是一个不寻常的事件转变, C#4规范并不能certificate两个空文字比较中特别提到的行为 。 事实上,严格阅读规范说“null == null”应该会产生歧义错误! (这是因为在准备C#3时清理C#2规范期间出现了编辑错误;规范作者并不打算将其视为非法。)

如果你不相信我,请仔细阅读规范。 它表示在int,uint,long,ulong,bool,decimal,double,float,string,enums,delegates和objects上定义了相等的运算符,以及所有值类型运算符的提升到可空的版本。

现在,我们立即遇到了问题; 这个集合无限大。 在实践中,我们不会在所有可能的委托和枚举类型上形成所有运算符的无限集合。 需要在此处修复规范,以注意添加到候选集的枚举和委托类型上的唯一运算符是枚举或委托类型的那些,它们是任一参数的类型。

因此,我们将枚举和委托类型从中删除,因为这两个参数都没有类型。

我们现在有一个重载解决问题; 我们必须首先消除所有不适用的运营商,然后确定最适合的运营商。

显然,在所有非可空值类型上定义的运算符都不适用。 这使得运算符处于可空值的类型,字符串和对象上。

我们现在可以出于“更好”的原因消除一些。 更好的运营商是具有更具体类型的运营商。 诠释? 比任何其他可空数字类型更具体,因此所有这些都被删除。 字符串比对象更具体,因此消除了对象。

那为string,int留下了相等的运算符? 和布尔? 作为适用的运营商。 哪一个是最好的? 它们都不比另一个好 。 因此,这应该是一个模糊错误。

为了通过规范certificate这种行为,我们必须修改规范,注意“null == null”被定义为具有字符串相等的语义,并且它是编译时常量true。

我昨天刚刚发现了这个事实; 你应该问多少奇怪的事。

要回答其他答案中提出的问题,为什么null >= null会给出关于int比较的警告? – 好吧,应用与我刚才相同的分析。 非可空值类型的>=运算符是不适用的,而剩下的运算符是int上的运算符? 是最好的。 >=没有歧义错误,因为在bool上没有定义>=运算符? 或字符串。 编译器正确地将运算符分析为两个可为空的int的比较。

要回答有关为什么运算符处于空 (而不是文字 )的特定exception行为的更一般性问题, 请参阅我对重复问题的回答 。 它清楚地解释了certificate这一决定的设计标准。 简而言之:对null的操作应该具有“我不知道”操作的语义。 您不知道的数量是否大于或等于您不知道的其他数量? 唯一明智的答案是“我不知道!” 但我们需要把它变成一个布尔,而明智的布尔是“假的”。 但是在比较相等时,大多数人认为null应该等于null,即使比较两个你不知道平等的事情也应该导致“我不知道”。 这个设计决定是将许多不良结果相互抵消以找到使该特征有效的最不良结果的结果; 它确实使语言有些不一致,我同意。

编译器推断,在比较运算符的情况下, null被隐式输入为int?

 Console.WriteLine(null == null); // true Console.WriteLine(null != null); // false Console.WriteLine(null < null); // false* Console.WriteLine(null <= null); // false* Console.WriteLine(null > null); // false* Console.WriteLine(null >= null); // false* 

Visual Studio提供警告:

*与’int?’类型的null比较 总是产生’假’

这可以使用以下代码进行validation:

 static void PrintTypes(LambdaExpression expr) { Console.WriteLine(expr); ConstantExpression cexpr = expr.Body as ConstantExpression; if (cexpr != null) { Console.WriteLine("\t{0}", cexpr.Type); return; } BinaryExpression bexpr = expr.Body as BinaryExpression; if (bexpr != null) { Console.WriteLine("\t{0}", bexpr.Left.Type); Console.WriteLine("\t{0}", bexpr.Right.Type); return; } return; } PrintTypes((Expression>)(() => null == null)); // constant folded directly to bool PrintTypes((Expression>)(() => null != null)); // constant folded directly to bool PrintTypes((Expression>)(() => null < null)); PrintTypes((Expression>)(() => null <= null)); PrintTypes((Expression>)(() => null > null)); PrintTypes((Expression>)(() => null >= null)); 

输出:

 () => True System.Boolean () => False System.Boolean () => (null < null) System.Nullable`1[System.Int32] System.Nullable`1[System.Int32] () => (null <= null) System.Nullable`1[System.Int32] System.Nullable`1[System.Int32] () => (null > null) System.Nullable`1[System.Int32] System.Nullable`1[System.Int32] () => (null >= null) System.Nullable`1[System.Int32] System.Nullable`1[System.Int32] 

为什么?

这对我来说似乎合情合理。 首先,这里是C#4.0规范的相关部分。

nullliteral§2.4.4.6:

null-literal可以隐式转换为引用类型或可空类型。

二进制数字促销§7.3.6.2:

对于预定义的+, – ,*,/,%,&,|,^,==,!=,>,<,> =和<=二元运算符的操作数,会发生二进制数字提升。 二进制数字促销隐式地将两个操作数转换为公共类型,在非关系运算符的情况下,它也成为操作的结果类型。 二进制数字促销包括按照它们在此处显示的顺序应用以下规则:

•如果任一操作数的类型为十进制,则另一个操作数将转换为十进制类型,或者如果另一个操作数的类型为float或double,则会发生绑定时错误。
•否则,如果任一操作数的类型为double,则另一个操作数将转换为double类型。
•否则,如果任一操作数的类型为float,则另一个操作数将转换为float类型。
•否则,如果任一操作数的类型为ulong,则另一个操作数将转换为ulong类型,或者如果另一个操作数的类型为sbyte,short,int或long,则会发生绑定时错误。
•否则,如果任一操作数的类型为long,则另一个操作数将转换为long类型。
•否则,如果任一操作数的类型为uint而另一个操作数的类型为sbyte,short或int,则两个操作数都将转换为long类型。
•否则,如果任一操作数的类型为uint,则另一个操作数将转换为uint类型。
•否则,两个操作数都将转换为int类型。

解除运营商§7.3.7:

提升的运算符允许在非可空值类型上运行的预定义和用户定义的运算符也可以与这些类型的可空forms一起使用。 提升运算符由满足特定要求的预定义和用户定义的运算符构成,如下所述:

•对于关系运营商
<> <=> =
如果操作数类型都是非可空值类型并且结果类型是bool,则存在提升forms的运算符。 提升forms是通过添加一个? 每个操作数类型的修饰符。 如果一个或两个操作数为空,则提升的运算符将生成值false。 否则,提升的运算符解包操作数并应用基础运算符以产生bool结果。

单独的null-literal实际上没有类型。 它是由它分配的内容推断出来的。 但是,这里没有任何转让。 只考虑具有语言支持的内置类型(具有关键字的内置类型), object或任何可空的将是一个很好的候选者。 然而, object不具有可比性,因此它被排除在外。 这使得可空类型成为好的候选人。 但是哪种? 由于左操作数和右操作数都没有指定的类型,因此默认情况下它们将转换为(可空) int 。 由于两个可空值都为null,因此返回false。

似乎编译器将null视为整数类型。 VS2008备注: "Comparing with null of type 'int?' always produces 'false'" "Comparing with null of type 'int?' always produces 'false'"

这是因为编译器足够智能,可以确定>= null始终为false,并使用常量值false替换表达式。

看看这个例子:

 using System; class Example { static void Main() { int? i = null; Console.WriteLine(i >= null); Console.WriteLine(i == null); } } 

这将编译为以下代码:

 class Example { private static void Main() { int? i = new int?(); Console.WriteLine(false); Console.WriteLine(!i.HasValue); } } 

当我跑

 null >= null 

我收到警告说:

与’int?’类型的null比较 总是产生’假’

我想知道为什么它会被转换为int。

这不是“官方”的答案,这是我最好的猜测。 但是如果你正在处理可空的int并比较它们,那么如果你处理两个为null的“int?”,你很可能总是希望比较返回false。 这样,如果它返回true,你可以确定你实际上比较了两个整数,而不是两个空值。 它只是不需要单独的空检查。

也就是说,如果不是你期望的行为,它可能会令人困惑!

此页面中记录了有关Nullable Types的行为。 它没有给出原因的真实解释,但我的解释如下。 ><没有关于null意思。 null是缺少值,因此仅等于另一个也没有值的变量。 由于><没有意义,因此转移到>=<=

他们似乎在混合使用C和SQL的范例。

在可空变量的上下文中,null == null应该确实产生错误,因为如果两个值都不知道则相等没有意义,但是对于C#作为整体执行此操作会在比较引用时引起问题。

简短的回答是“因为这就是规范中定义运算符的方式”。

从ECMA C#规范的 8.19节:

提升forms的==!=运算符认为两个空值相等,并且空值不等于非空值。 如果一个或两个操作数为空,则<><=>=运算符的提升forms返回false。

这个问题是一个似曾相识,哦,等在这里……

当= =为空值返回true时,为什么> =返回false?

我从另一个答案中记得的是:

因为Equality与可比性分开定义。 你可以测试x == null但x> null是没有意义的。 在C#中,它总是错误的。