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.