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?
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 valuefalse
if one or both operands arenull
. Otherwise, the lifted operator unwraps the operands and applies the underlying operator to produce thebool
result.
In particular, this means that the usual laws of relations don't hold; x >= y
does not imply !(x < y)
.
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.
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.
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 string
s operators are not included here).
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?
.
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.
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.
It seems like the compiler treats null as if they're integer types.
VS2008 remarks: "Comparing with null of type 'int?' always produces 'false'"
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);
}
}
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With