41

为什么在.NET中

null >= null

解析为假,但是

null == null 

解析为真的?

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

有人有官方答案吗?

4

11 回答 11

34

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

对于关系运算符

< > <= >=

如果操作数类型都是不可为空的值类型并且结果类型是 ,则存在运算符的提升形式bool。提升形式是通过向每个操作数类型添加单个?修饰符来构造的。如果一个或两个操作数是,则提升的运算符产生值falsenull。否则,提升的运算符会展开操作数并应用底层运算符来生成bool结果。

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

血腥细节

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

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

  1. 首先,检查用户定义的运算符的适用性。这是通过检查>=... 每一侧的类型定义的运算符来完成的,这引发了类型null是什么的问题!在null给定一个之前,文字实际上没有任何类型,它只是“空文字”。按照 14.2.5 下的说明,我们发现这里没有合适的运算符,因为 null 文字没有定义任何运算符。

  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?

于 2011-01-19T01:15:20.687 回答
27

许多答案都符合规范。结果证明这是一个不寻常的事件,C# 4 规范没有证明特别提到的比较两个 null 文字的行为是合理的。事实上,严格阅读规范说“null == null”应该会产生歧义错误!(这是由于在清理 C# 2 规范以准备 C# 3 期间发生的编辑错误;规范作者无意将此设为非法。)

如果您不相信我,请仔细阅读规范。它说在 int、uint、long、ulong、bool、decimal、double、float、string、枚举、委托和对象上定义了相等运算符,以及所有值类型运算符的提升为可空的版本。

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

因此,让我们将枚举和委托类型排除在外,因为这两个参数都没有类型。

我们现在有一个重载解决问题;我们必须首先排除所有不适用的算子,然后确定最佳的适用算子。

显然,在所有不可为空的值类型上定义的运算符都不适用。这就留下了可空值类型、字符串和对象上的运算符。

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

这留下了字符串的相等运算符,int?和布尔?作为适用的运营商。哪一个是最好的? 没有一个比另一个更好。因此,这应该是一个模棱两可的错误。

为了让规范证明这种行为是合理的,我们将不得不修改规范以注意“null == null”被定义为具有字符串相等的语义,并且它是编译时常量 true。

其实我昨天才发现这个事实;你应该问它是多么奇怪。

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

要回答有关为什么 null(而不是文字)上的运算符具有特殊异常行为的更一般性问题,请参阅我对重复问题的回答. 它清楚地解释了证明这一决定的设计标准。简而言之:对 null 的操作应该具有对“我不知道”的操作的语义。你不知道的数量是否大于或等于另一个你不知道的数量?唯一明智的答案是“我不知道!” 但是我们需要把它变成一个布尔值,而合理的布尔值是“假的”。但是在比较相等时,大多数人认为 null 应该等于 null,即使比较您不知道的两个事物是否相等也应该导致“我不知道”。这种设计决策是权衡许多不良结果以找到使该功能发挥作用的最不坏的结果的结果;我同意,这确实使语言有些不一致。

于 2011-01-19T02:55:40.037 回答
5

编译器推断,在比较运算符的情况下,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比较?总是产生“假”

这可以使用以下代码进行验证:

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<Func<bool>>)(() => null == null)); // constant folded directly to bool
PrintTypes((Expression<Func<bool>>)(() => null != null)); // constant folded directly to bool
PrintTypes((Expression<Func<bool>>)(() => null < null));
PrintTypes((Expression<Func<bool>>)(() => null <= null));
PrintTypes((Expression<Func<bool>>)(() => null > null));
PrintTypes((Expression<Func<bool>>)(() => 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 Spec的相关部分。

空文字§2.4.4.6:

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

二进制数字促销§7.3.6.2:

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

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

提升运营商§7.3.7:

提升的运算符允许对不可为空的值类型进行操作的预定义和用户定义的运算符也与这些类型的可空形式一起使用。提升运算符由满足某些要求的预定义和用户定义的运算符构成,如下所述:

• 对于关系运算符
< > <= >=
,如果操作数类型都是非空值类型并且结果类型是bool,则存在运算符的提升形式。提升的形式是通过添加单个 ? 每个操作数类型的修饰符。如果一个或两个操作数为空,则提升的运算符产生值 false。否则,提升的运算符会展开操作数并应用底层运算符来生成 bool 结果。

单独的 null-literal 并没有真正的类型。它是由它被分配给什么来推断的。然而,这里没有分配。仅考虑具有语言支持的内置类型(带有关键字的类型)object或任何可为空的类型都是不错的选择。但是object没有可比性,因此被排除在外。这留下了可空类型很好的候选者。但哪种类型?由于左操作数和右操作数都没有指定类型,因此int默认情况下它们被转换为(可为空的)。由于两个可为 null 的值都是 null,因此它返回 false。

于 2011-01-19T02:44:34.003 回答
3

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

于 2011-01-19T00:50:47.453 回答
2

当我跑

null >= null

我收到一条警告说:

与 'int?' 类型的 null 比较 总是产生“假”

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

于 2011-01-19T00:50:19.163 回答
2

这是因为编译器足够聪明,可以确定它始终>= null为假,并将您的表达式替换为.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);
    }
}
于 2011-01-19T00:53:46.720 回答
2

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

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

于 2011-01-19T00:58:07.453 回答
2

该行为记录在此页面中关于Nullable Types。它没有给出真正的解释为什么,但我的解释如下。>并且<对于 没有任何意义nullnull是没有值,因此仅等于另一个也没有值的变量。>由于and没有任何意义<,因此将其延续到>=and <=

于 2011-01-19T01:18:10.200 回答
2

他们似乎混合了 C 和 SQL 的范例。

在可空变量的上下文中,null == null 应该真正产生 false,因为如果两个值都不知道,则相等是没有意义的,但是对于整个 C# 这样做会在比较引用时导致问题。

于 2011-01-19T01:24:48.130 回答
1

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

ECMA C# 规范的第 8.19 节:

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

于 2011-01-19T01:22:52.877 回答
1

这个问题似曾相识,哦,等等,它是......

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

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

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

于 2011-01-19T01:58:16.537 回答