C#/.NET Little Wonders: The Nullable static class
- by James Michael Hare
Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders posts can be found here. Today we’re going to look at an interesting Little Wonder that can be used to mitigate what could be considered a Little Pitfall. The Little Wonder we’ll be examining is the System.Nullable static class. No, not the System.Nullable<T> class, but a static helper class that has one useful method in particular that we will examine… but first, let’s look at the Little Pitfall that makes this wonder so useful. Little Pitfall: Comparing nullable value types using <, >, <=, >= Examine this piece of code, without examining it too deeply, what’s your gut reaction as to the result? 1: int? x = null;
2:
3: if (x < 100)
4: {
5: Console.WriteLine("True, {0} is less than 100.",
6: x.HasValue ? x.ToString() : "null");
7: }
8: else
9: {
10: Console.WriteLine("False, {0} is NOT less than 100.",
11: x.HasValue ? x.ToString() : "null");
12: }
Your gut would be to say true right? It would seem to make sense that a null integer is less than the integer constant 100. But the result is actually false! The null value is not less than 100 according to the less-than operator.
It looks even more outrageous when you consider this also evaluates to false:
1: int? x = null;
2:
3: if (x < int.MaxValue)
4: {
5: // ...
6: }
So, are we saying that null is less than every valid int value? If that were true, null should be less than int.MinValue, right? Well… no:
1: int? x = null;
2:
3: // um... hold on here, x is NOT less than min value?
4: if (x < int.MinValue)
5: {
6: // ...
7: }
So what’s going on here? If we use greater than instead of less than, we see the same little dilemma:
1: int? x = null;
2:
3: // once again, null is not greater than anything either...
4: if (x > int.MinValue)
5: {
6: // ...
7: }
It turns out that four of the comparison operators (<, <=, >, >=) are designed to return false anytime at least one of the arguments is null when comparing System.Nullable wrapped types that expose the comparison operators (short, int, float, double, DateTime, TimeSpan, etc.). What’s even odder is that even though the two equality operators (== and !=) work correctly, >= and <= have the same issue as < and > and return false if both System.Nullable wrapped operator comparable types are null!
1: DateTime? x = null;
2: DateTime? y = null;
3:
4: if (x <= y)
5: {
6: Console.WriteLine("You'd think this is true, since both are null, but it's not.");
7: }
8: else
9: {
10: Console.WriteLine("It's false because <=, <, >, >= don't work on null.");
11: }
To make matters even more confusing, take for example your usual check to see if something is less than, greater to, or equal:
1: int? x = null;
2: int? y = 100;
3:
4: if (x < y)
5: {
6: Console.WriteLine("X is less than Y");
7: }
8: else if (x > y)
9: {
10: Console.WriteLine("X is greater than Y");
11: }
12: else
13: {
14: // We fall into the "equals" assumption, but clearly null != 100!
15: Console.WriteLine("X is equal to Y");
16: }
Yes, this code outputs “X is equal to Y” because both the less-than and greater-than operators return false when a Nullable wrapped operator comparable type is null. This violates a lot of our assumptions because we assume is something is not less than something, and it’s not greater than something, it must be equal.
So keep in mind, that the only two comparison operators that work on Nullable wrapped types where at least one is null are the equals (==) and not equals (!=) operators:
1: int? x = null;
2: int? y = 100;
3:
4: if (x == y)
5: {
6: Console.WriteLine("False, x is null, y is not.");
7: }
8:
9: if (x != y)
10: {
11: Console.WriteLine("True, x is null, y is not.");
12: }
Solution: The Nullable static class
So we’ve seen that <, <=, >, and >= have some interesting and perhaps unexpected behaviors that can trip up a novice developer who isn’t expecting the kinks that System.Nullable<T> types with comparison operators can throw. How can we easily mitigate this?
Well, obviously, you could do null checks before each check, but that starts to get ugly:
1: if (x.HasValue)
2: {
3: if (y.HasValue)
4: {
5: if (x < y)
6: {
7: Console.WriteLine("x < y");
8: }
9: else if (x > y)
10: {
11: Console.WriteLine("x > y");
12: }
13: else
14: {
15: Console.WriteLine("x == y");
16: }
17: }
18: else
19: {
20: Console.WriteLine("x > y because y is null and x isn't");
21: }
22: }
23: else if (y.HasValue)
24: {
25: Console.WriteLine("x < y because x is null and y isn't");
26: }
27: else
28: {
29: Console.WriteLine("x == y because both are null");
30: }
Yes, we could probably simplify this logic a bit, but it’s still horrendous! So what do we do if we want to consider null less than everything and be able to properly compare Nullable<T> wrapped value types?
The key is the System.Nullable static class. This class is a companion class to the System.Nullable<T> class and allows you to use a few helper methods for Nullable<T> wrapped types, including a static Compare<T>() method of the.
What’s so big about the static Compare<T>() method? It implements an IComparer compatible comparison on Nullable<T> types. Why do we care? Well, if you look at the MSDN description for how IComparer works, you’ll read:
Comparing null with any type is allowed and does not generate an exception when using IComparable. When sorting, null is considered to be less than any other object.
This is what we probably want! We want null to be less than everything! So now we can change our logic to use the Nullable.Compare<T>() static method:
1: int? x = null;
2: int? y = 100;
3:
4: if (Nullable.Compare(x, y) < 0)
5: {
6: // Yes! x is null, y is not, so x is less than y according to Compare().
7: Console.WriteLine("x < y");
8: }
9: else if (Nullable.Compare(x, y) > 0)
10: {
11: Console.WriteLine("x > y");
12: }
13: else
14: {
15: Console.WriteLine("x == y");
16: }
Summary
So, when doing math comparisons between two numeric values where one of them may be a null Nullable<T>, consider using the System.Nullable.Compare<T>() method instead of the comparison operators. It will treat null less than any value, and will avoid logic consistency problems when relying on < returning false to indicate >= is true and so on.
Tweet
Technorati Tags: C#,C-Sharp,.NET,Little Wonders,Little Pitfalls,Nulalble