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

时间:2011-01-19 00:43:00

标签: c# .net null comparison-operators

为什么在.NET中

null >= null

解析为false,但

null == null 

解析为真?

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

有没有人有正式答案?

11 个答案:

答案 0 :(得分:30)

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

  

对于关系运算符

< > <= >=
     

如果操作数类型都是不可为空的值类型,并且结果类型如此,则存在提升形式的运算符   是bool。通过向每个操作数类型添加单个?修饰符来构造提升形式。 解除了   如果一个或两个操作数都是false ,则运算符生成值null。否则,抬起的操作员会解开   操作数并应用基础运算符来生成bool结果。

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

血腥细节

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

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

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

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

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

  4. 实际上执行此分辨率将非常繁琐,但幸运的是有一条捷径。在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?

答案 1 :(得分:26)

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

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

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

因此,让enum和委托类型不在其中,因为这两个参数都没有类型。

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

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

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

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

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

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

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

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

答案 2 :(得分: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的相关部分。

nullliteral§2.4.4.6:

  

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

二进制数字促销§7.3.6.2:

  

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

     

•如果任一操作数的类型为十进制,则另一个操作数将转换为十进制类型,或者如果另一个操作数的类型为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:

  

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

     

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

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

答案 3 :(得分:3)

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

答案 4 :(得分:2)

当我跑步时

null >= null

我收到警告说:

  

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

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

答案 5 :(得分: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);
    }
}

答案 6 :(得分:2)

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

那就是说,如果不是你期望的行为,那可能会让人感到困惑!

答案 7 :(得分:2)

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

答案 8 :(得分:2)

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

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

答案 9 :(得分:1)

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

来自ECMA C# spec的第8.19节:

  

提升==!=的形式   运算符考虑两个空值   等于,并且空值不等于a   非空值。提升形式的   <><=>=运营商   如果一个或两个操作数返回false   是空的。

答案 10 :(得分:1)

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

Why does >= return false when == returns true for null values?

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

  

因为Equality是单独定义的   来自可比性。你可以测试x ==   null但x> null毫无意义。在   C#总是假的。