C# 可空相等运算,为什么 null <= null 解析为 false?

发布于 2024-10-12 09:32:25 字数 248 浏览 4 评论 0 原文

为什么在 .NET 中

null >= null

解析为 false,但

null == null 

解析为 true?

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

有谁有官方的答案吗?

Why is it that in .NET

null >= null

resolves as false, but

null == null 

resolves as true?

In other words, why isn't null >= null equivalent to null > null || null == null?

Does anyone have the official answer?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

◇流星雨 2024-10-19 09:32:26

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

为什么当 == 对于 null 值返回 true 时,>= 返回 false?

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

因为平等是单独定义的
来自可比性。你可以测试x==
空但 x > null 是没有意义的。在
C# 它总是错误的。

This question was a Deja vu, oh wait here it is...

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

The thing I remember from the other answer was :

Because Equality is defined separately
from Comparability. You can test x ==
null but x > null is meaningless. In
C# it will always be false.

糖粟与秋泊 2024-10-19 09:32:25

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

对于关系运算符

<前><代码>< > <=>=

如果操作数类型均为不可空值类型并且结果类型,则存在运算符的提升形式
是布尔值。提升形式是通过向每个操作数类型添加单个 ? 修饰符来构造的。 举起的
如果一个或两个操作数为 null
,则运算符会生成值 false。否则,提升的操作员会打开包装
操作数并应用底层运算符来生成 bool 结果。

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

血淋淋的细节

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

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

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

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

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

实际上执行这个解决方案会非常乏味,但幸运的是有一个捷径。第 14.2.6 节给出了重载决策结果的信息示例,其中指出:

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

int 运算符 *(int x, int y);
uint 运算符 *(uint x, uint y);
长运算符*(长x,长y);
ulong 运算符 *(ulong x, ulong y);
void 运算符 *(long x, ulong y);
void 运算符 *(ulong x, long y);
浮点运算符 *(浮点 x, 浮点 y);
双运算符 *(双 x, 双 y);
小数运算符*(小数x,小数y);

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

由于两边都是null,我们可以立即抛出所有未提升的运算符。这给我们留下了所有原始数字类型的提升数字运算符。

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

This behaviour is defined in the C# specification (ECMA-334) in section 14.2.7 (I have highlighted the relevant part):

For the relational operators

< > <= >=

a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type
is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted
operator produces the value false if one or both operands are null
. Otherwise, the lifted operator unwraps
the operands and applies the underlying operator to produce the bool result.

In particular, this means that the usual laws of relations don't hold; x >= y does not imply !(x < y).

Gory details

Some people have asked why the compiler decides that this is a lifted operator for int? in the first place. Let's have a look. :)

We start with 14.2.4, 'Binary operator overload resolution'. This details the steps to follow.

  1. First, the user-defined operators are examined for suitability. This is done by examining the operators defined by the types on each side of >=... which raises the question of what the type of null is! The null literal actually doesn't have any type until given one, it's simply the "null literal". By following the directions under 14.2.5 we discover there are no operators suitable here, since the null literal doesn't define any operators.

  2. This step instructs us to examine the set of predefined operators for suitability. (Enums are also excluded by this section, since neither side is an enum type.) The relevant predefined operators are listed in sections 14.9.1 to 14.9.3, and they are all operators upon primitive numeric types, along with the lifted versions of these operators (note that strings operators are not included here).

  3. Finally, we must perform overload resolution using these operators and the rules in 14.4.2.

Actually performing this resolution would be extremely tedious, but luckily there is a shortcut. Under 14.2.6 there is an informative example given of the results of overload resolution, which states:

...consider the predefined implementations of the binary * operator:

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);

When overload resolution rules (§14.4.2) are applied to this set of operators, the effect is to select the first of
the operators for which implicit conversions exist from the operand types.

Since both sides are null we can immediately throw out all unlifted operators. This leaves us with the lifted numeric operators on all primitive numeric types.

Then, using the previous information, we select the first of the operators for which an implicit conversion exists. Since the null literal is implicitly convertible to a nullable type, and a nullable type exists for int, we select the first operator from the list, which is int? >= int?.

我的鱼塘能养鲲 2024-10-19 09:32:25

许多答案都符合规范。事实证明这是一个不寻常的事件,C# 4 规范没有证明特别提到的比较两个空文字的行为的合理性。事实上,严格阅读规范表明“null == null”应该给出歧义错误! (这是由于在为 C# 3 做准备的 C# 2 规范清理过程中发生的编辑错误造成的;规范作者无意将其定为非法。)

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

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

因此,我们将枚举和委托类型保留在其中,因为这两个参数都没有类型。

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

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

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

那么剩下字符串、整数的相等运算符吗?和布尔?作为适用的运营商。哪一个最好? 没有一个比另一个更好。因此这应该是一个歧义错误。

为了使这种行为在规范中得到证实,我们必须修改规范,以注意“null == null”被定义为具有字符串相等的语义,并且它是编译-时间常数为真。

事实上我昨天才发现这个事实;你居然问这个,真是奇怪。

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

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

A number of answers appeal to the spec. In what turns out to be an unusual turn of events, the C# 4 spec does not justify the behaviour specifically mentioned of comparison of two null literals. In fact, a strict reading of the spec says that "null == null" should give an ambiguity error! (This is due to an editing error made during the cleanup of the C# 2 specification in preparation for C# 3; it is not the intention of the spec authors to make this illegal.)

Read the spec carefully if you don't believe me. It says that there are equality operators defined on int, uint, long, ulong, bool, decimal, double, float, string, enums, delegates and objects, plus the lifted-to-nullable versions of all the value type operators.

Now, immediately we have a problem; this set is infinitely large. In practice we do not form the infinite set of all operators on all possible delegate and enum types. The spec needs to be fixed up here to note that the only operators on enum and delegate types which are added to the candidate sets are those of enum or delegate types that are the types of either argument.

Let's therefore leave enum and delegate types out of it, since neither argument has a type.

We now have an overload resolution problem; we must first eliminate all the inapplicable operators, and then determine the best of the applicable operators.

Clearly the operators defined on all the non-nullable value types are inapplicable. That leaves the operators on the nullable value types, and string, and object.

We can now eliminate some for reasons of "betterness". The better operator is the one with the more specific types. int? is more specific than any of the other nullable numeric types, so all of those are eliminated. String is more specific than object, so object is eliminated.

That leaves equality operators for string, int? and bool? as the applicable operators. Which one is the best? None of them is better than the other. Therefore this should be an ambiguity error.

For this behaviour to be justified by the spec we are going to have to emend the specification to note that "null == null" is defined as having the semantics of string equality, and that it is the compile-time constant true.

I actually just discovered this fact yesterday; how odd that you should ask about it.

To answer the questions posed in other answers about why null >= null gives a warning about comparisons to int? -- well, apply the same analysis as I just did. The >= operators on non-nullable value types are inapplicable, and of the ones that are left, the operator on int? is the best. There is no ambiguity error for >= because there is no >= operator defined on bool? or string. The compiler is correctly analyzing the operator as being comparison of two nullable ints.

To answer the more general question about why operators on null values (as opposed to literals) have a particular unusual behaviour, see my answer to the duplicate question. It clearly explains the design criteria that justify this decision. In short: operations on null should have the semantics of operations on "I don't know". Is a quantity you don't know greater than or equal to another quantity you don't know? The only sensible answer is "I don't know!" But we need to turn that into a bool, and the sensible bool is "false". But when comparing for equality, most people think that null should be equal to null even though comparing two things that you don't know for equality should also result in "I don't know". This design decision is the result of trading off many undesirable outcomes against one another to find the least bad one that makes the feature work; it does make the language somewhat inconsistent, I agree.

苄①跕圉湢 2024-10-19 09:32:25

编译器推断,在比较运算符的情况下,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 规范

空文字§2.4.4.6:

空文字可以隐式转换为引用类型或可为空类型。

二进制数字促销§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:

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

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

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

The compiler is inferring that in the case of the comparison operators, the null is being implicitly typed as 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 offers a warning:

*Comparing with null of type 'int?' always produces 'false'

This can be verified with the following code:

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));

Outputs:

() => 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]

Why?

This seems logical to me. First of all, here's relevant sections of the C# 4.0 Spec.

The null literal §2.4.4.6:

The null-literal can be implicitly converted to a reference type or nullable type.

Binary numeric promotions §7.3.6.2:

Binary numeric promotion occurs for the operands of the predefined +, –, *, /, %, &, |, ^, ==, !=, >, <, >=, and <= binary operators. Binary numeric promotion implicitly converts both operands to a common type which, in case of the non-relational operators, also becomes the result type of the operation. Binary numeric promotion consists of applying the following rules, in the order they appear here:

• If either operand is of type decimal, the other operand is converted to type decimal, or a binding-time error occurs if the other operand is of type float or double.
• Otherwise, if either operand is of type double, the other operand is converted to type double.
• Otherwise, if either operand is of type float, the other operand is converted to type float.
• Otherwise, if either operand is of type ulong, the other operand is converted to type ulong, or a binding-time error occurs if the other operand is of type sbyte, short, int, or long.
• Otherwise, if either operand is of type long, the other operand is converted to type long.
• Otherwise, if either operand is of type uint and the other operand is of type sbyte, short, or int, both operands are converted to type long.
• Otherwise, if either operand is of type uint, the other operand is converted to type uint.
• Otherwise, both operands are converted to type int.

Lifted operators §7.3.7:

Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following:

• For the relational operators
< > <= >=
a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator produces the value false if one or both operands are null. Otherwise, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.

The null-literal alone doesn't really have a type. It is inferred by what it is assigned to. No assignment takes place here however. Only considering built-in types with language support (ones with keywords), object or any nullable would be a good candidate. However object isn't comparable so it gets ruled out. That leaves nullable types good candidates. But which type? Since neither left nor right operands has a specified type, they are converted to a (nullable) int by default. Since both nullable values are null, it returns false.

别念他 2024-10-19 09:32:25

编译器似乎将 null 视为整数类型。
VS2008备注:“与'int'类型的null比较?”总是产生“假””

It seems like the compiler treats null as if they're integer types.
VS2008 remarks: "Comparing with null of type 'int?' always produces 'false'"

蓝颜夕 2024-10-19 09:32:25

这是因为编译器足够聪明,可以发现 >= 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);
    }
}

This is because the compiler is smart enough to figure out that >= null will always be false and replaces your expression with a constant value of false.

Check out this example:

using System;

class Example
{
    static void Main()
    {
        int? i = null;

        Console.WriteLine(i >= null);
        Console.WriteLine(i == null);
    }
}

This compiles down to the following code:

class Example
{
    private static void Main()
    {
        int? i = new int?();
        Console.WriteLine(false);
        Console.WriteLine(!i.HasValue);
    }
}
公布 2024-10-19 09:32:25

当我跑步时,

null >= null

我收到一条警告:

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

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

When I run

null >= null

I get a warning saying:

Comparing with null of type 'int?'
always produces 'false'

I wonder why it is casted to an int though.

撞了怀 2024-10-19 09:32:25

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

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

This isn't the "official" answer, it's my best guess. But if you're dealing with nullable ints and comparing them, you most likely always want the comparison to return false if you're dealing with two "int?"s that are null. That way if it returns true, you can be sure you've actually compared two integers, not two null values. It just removes the need for a separate null check.

That said, it is potentially confusing if it's not the behaviour you expect!

浅唱々樱花落 2024-10-19 09:32:25

此行为记录在有关可空类型的页面中。它没有给出真正的解释,但我的解释如下。对于 null 而言,>< 没有任何意义。 null 表示没有值,因此仅等于另一个也没有值的变量。由于 >< 没有任何意义,因此将其转入 >=<=.

The behavior is documented in this page about Nullable Types. It doesn't give a real explanation for why, but my interpretation is the following. > and < have no meaning when with regards to null. null is the absence of a value and thus is only equal to another variable that also has no value. Since there is no meaning for > and <, that is carried over to >= and <=.

谁人与我共长歌 2024-10-19 09:32:25

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

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

They seem to be mixing paradigms from C and SQL.

In the context of nullable variables, null == null should really yield false since equality makes no sense if neither value is known, however doing that for C# as a whole would cause issues when comparing references.

源来凯始玺欢你 2024-10-19 09:32:25

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

来自 ECMA C# 规范 的第 8.19 节:

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

The short answer would be "because that's the way those operators are defined in the specification".

From section 8.19 of the ECMA C# spec:

Lifted forms of the == and !=
operators consider two null values
equal, and a null value unequal to a
non-null value. Lifted forms of the
<, >, <=, and >= operators
return false if one or both operands
are null.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文