One of the C# oddities I noted in my recent article was that I find it odd that creating a numeric type with less-than, greater-than, and similar operators requires implementing a lot of redundant methods, methods whose values could be deduced by simply implementing a comparator. For an example of such, see my previous series of articles on implementing math from scratch.

A number of people commented that my scheme does not work in a world with nullable arithmetic (or, similarly, NaN semantics, which are similar enough that I’m not going to call out the subtle differences here.) That reminded me that I’d been intending for some time to point out that when it comes to comparison operators, nullable arithmetic is deeply weird. Check out this little program:

using System; using System.Collections.Generic; class P { static void Main() { int? one = 1; int? nll = null; var comparer = Comparer<int?>.Default; Console.WriteLine(one < nll); // false Console.WriteLine(one > nll); // false Console.WriteLine(one <= nll); // false Console.WriteLine(one >= nll); // false Console.WriteLine(one == nll); // false Console.WriteLine(one != nll); // true

It is possible for two things to be unequal, but neither is greater than or less than the other. Nulls are unordered with respect to non-nulls.

Console.WriteLine(nll < nll); // false Console.WriteLine(nll > nll); // false Console.WriteLine(nll <= nll); // false Console.WriteLine(nll >= nll); // false Console.WriteLine(nll == nll); // true Console.WriteLine(nll != nll); // false

Two things can be equal to each other, but not greater-than-or-equal to each other! Apparently greater-than-or-equal does not actually have “or” semantics.

Console.WriteLine(comparer.Compare(one, nll)); // one is greater } }

And finally, the default comparer contradicts the greater-than and less-than operators. It says that one is greater than null, but the operators say that neither is the greater.

Basically the rule here is that nulls compare for equality normally, but any other comparison results in false. This is a bit weird, but it does make some sense. The language designers have a number of possible choices to make.

Make comparison operators produce nullable bool. If there is a null operand, the result is null, not false. Note that this implies that x == null should always produce null, contrary to the expectation of users familiar with reference null comparisons.

should always produce null, contrary to the expectation of users familiar with reference null comparisons. Make comparison operators produce bool, and say that greater-than-or-equal comparisons to null have the same semantics as “or-ing” together the greater-than and equals operations. Now we must consider whether the user who is doing a comparison involving nullables is really asking the question “is it the case that these two things (a) have equal values, or (b) have value x greater than value y, or (c) neither has a value?”. That does not seem like the semantics that a user has in their head when comparing two potentially null integers. If the question is “are the sales for March greater than or equal to the sales for April? and both figures are unknown, it seems wrong to say “yes, they are equally unknown.”

Make comparison operators produce a bool and apply a total ordering; null is smaller than everything other than itself, and greater than nothing.

Good heavens, what a mess. The latter is what you want for sorting; as we have discussed before, a sort comparison must produce a consistent total ordering, and that is what the default comparer does. The built-in operators do not provide a total ordering. Rather, they try to make a best-effort attempt at a reasonable compromise. The operators produce bools, equality semantics work as people expect, and non-equality comparisons involving null produce false, indicating that we do not have information about how the quantities are ordered.

What is the practical upshot here? First, be careful when comparing nullable numbers. Second, if you need to sort a bunch of possibly-null values, you cannot simply use the comparison operators. Instead you should use the default comparison object, as it produces a consistent total order with null at the bottom.