C# is a wonderful language for modern programming. While everything in C# has a reason and a place, occasionally, there are things that can be confusing for a developer who isn’t aware of what is happening behind the scenes. This is my third post in the Little Pitfalls series where I explore these small pitfalls; the previous Little Pitfall post can be found here.
In the last Little Wonders post, we talked about the Nullable static class (not to be confused with the Nullable<T> struct) and through that venue discussed some of the issues comparing nullable numeric valueswhich evaluate to null.
This week we will dive deeper into that discussion and discuss more fully the little pitfall deals of performing math or comparisons on System.Nullable<T> wrapped value type that support the arithmetic and/or comparison operator overloads
Overview: Nullable value types
Value types in of themselves cannot be null, however those same value types can be wrapped in a System.Nullable<T> wrapper (which in C# can be declared more simply by adding a ‘?’ behind the type) which then allows the variable to be assigned null. This allows you to treat a value type as “optional” where it may or may not have a value.
Note: A null nullable wrapped value type is not the same as a null reference to a reference type. The null nullable value type still has storage for the value type itself, it just has a bool flag indicating whether or not it has an assigned value. More on this in a future C#/.NET Fundamentals post!
An interesting feature of the System.Nullable<T> wrapper type is that you can directly use the operator overloads that are also available in the wrapped type. This is not just relevant for primitive types (like int, double, long, etc) but also operator overloads defined on struct types.
For example, let’s create just a simple struct to represent a fraction (never mind if this should truly be a struct or class in the grand scheme of things, just buy into it as an example):
1 : public struct Fraction 2 : {
3 : public int Numerator {
get;
set;
}
4 : public int Denominator {
get;
set;
}
5 : 6 : // an operator overload to support + operator.
7 : public static Fraction
operator+(Fraction first, Fraction second) 8 : {
9 : // ignore reduction and LCD, just doing brute force
10 : return new Fraction 11 : {
12 : Numerator = first.Numerator *
second.Denominator 13 : +second.Numerator *
first.Denominator,
14 : Denominator = first.Denominator * second.Denominator 15 :
};
16:
}
17:
}
So, we now defined the operator + for our Fraction value type. So this means we can add two fractions like:
1 : var oneHalf = new Fraction{Numerator = 1, Denominator = 2};
2 : var twoThirds = new Fraction{Numerator = 2, Denominator = 3};
3 : 4 : // 1/2 = 3/6, and 2/3 = 4/6, thus 1/2 + 2/3 = 7/6
5 : var sevenSixths = oneHalf + twoThirds;
In addition, the Nullable<T> wrapper allow any arithmetic and comparison operators of the wrapped type to be performed directly on the wrapper type without explicitly extracting the value (using the Value property):
1 : Fraction oneHalf = new Fraction{Numerator = 1, Denominator = 2};
2 : Fraction ? huh = null;
3 : 4 : // this is syntactically correct and thus compiles, but what does it do
// if huh is null?
5 : var whatTheHeck = oneHalf + huh;
So this leads us to our little pitfall. The above code not only compiles, it succeeds at runtime, though perhaps not in the way you may expect. Let’s dig deeper into what we’ll get…
Pitfall: Math with null yields…?
The first time you see math on a Nullable<T> wrapped value type, your first expectation may be to expect some sort of exception to be thrown. Let’s say we’re attempting to perform a volume calculation, and for whatever reason we choose to make our volume generic enough so it can handle a 2d Rectangle or a 3d Rectangular solid:
1 : int length = 20;
2 : int height = 30;
3 : 4 : // maybe allow width to be null if 2d or non-null if 3d
5 : int
? width = null;
6 : 7 : // calculate the volume
8 : var volume = length * height * width;
9 : 10 : Console.WriteLine("The volume was: " + volume);
Now, you notice that we are performing the mathematical multiplication (operator *) against two int values and a int? value. So, what happens when width is null as we see in the example above?
Well, you might expect that it would throw an exception, but actually it doesn’t. So then, if it doesn’t throw, does it evaluate to zero (as the coder above may have assumed)? No again, because null is undefined, it can’t really evaluate to any particular value – even zero.
So what happens? Well, you know the typical arithmetic type promotions (int + long = long, etc.) where basically math between a “smaller” type and a “larger” type promotes the “smaller” type to the “larger” type. Math between Nullable<T> wrapped types is somewhat similar in that anytime you have math between a type and a Nullable<T> wrapped type, the result will be a Nullable<T> wrapped instance of the “larger” type. That is, an expression with int? + long yields a long? result.
So this hints at our answer, which is: when performing math between a type and a Nullable<T> wrapped instance of a compatible type, the result is null! As they say: the good news is it fails gracefully (doesn’t throw), but the bad news is it fails gracefully (doesn’t throw). Not throwing is s nice in some ways, but in others it can lead to bugs that don’t appear in the trouble spot itself but may have negative effects later in the code.
If you noticed, we used implicit typing in our example above, this may have accidentally helped hide the issue at hand. If we try to type volume to an int, we get a compiler error that int? cannot be converted to int which is a big hint that the result of the expression got promoted to an int? result.
1 : // Compiler error, result of int * int * int? = int?
2 : int volume = length * height * width;
3 : 4 : // Correct, nullable math yields nullable values
5 : int
? volume = length * height * width;
So keep in mind, any time you do nullable math, the result of the whole operation will be null if any part is null. The following is logically equivalent to performing the original math:
1: // if a part of the expression is null, the math == null
2: int? volume = width.HasValue
3: ? (length * height * width.Value)
4: : (int?)null;
So be aware that math with a null Nullable<T> wrapped type yields null, so protect your calculations as appropriate, and take appropriate actions if any Nullable<T> wrapped value types are null (by checking the HasValue property or comparing to null) before you perform any operations on them.
Pitfall: Relational operators comparing null yield…?
Comparing a Nullable<T> wrapped value type using supported relational operators has a similar effect to the arithmetic operators discussed above, but with a little twist. When you do comparisons between a Nullable<T> wrapped type whose value is null and a compatible type, the result is always false for the >, >=, <, and <= operators. However, it will evaluate as expected for == and !=.
The <= and >= can be especially confusing for those who aren’t familiar with nullable logic when you consider this snippet:
1 : int ? x = null;
2 : int ? y = null;
3 : 4 : // you'd think since x == y this would evaluate to true...
5 : // but this is actually logically equivalent to:
6 : // if (x.HasValue && y.HasValue && x.Value <= y.Value)
7 : if (x <= y) 8 : {
9 : Console.WriteLine("You'd think since x == y this would be true...");
10:
}
11 : else 12 : {
13 : Console.WriteLine(
"But it's not, returns false for <= or >= if either arg null.");
14:
}
So, in a nutshell, any <, >, <=, or >= operators are undefined on null, because null has no true relative rank (though as we’ve seen in a previous post, the Nullable.Compare<T>() method takes a different approach and treats null as less than anything). If either of the operands to these operators are null then the result is false – even if both are null in the case of >=, and <=. But since == and != don’t imply order at all, they are just simple equality or non-equality, comparisons to null are well-defined and make sense, and thus will work as expected.
This can bite a bit if you are expecting an exception, but it fails quietly – and in this case even more quietly than Nullable<T> wrapped type’sexposed arithmetic operators! With the nullable math, we had the null result to let us know the result was undefined. However when doing a nullable comparison, instead of returning bool? whose value is null, it simply returns false which is indistinguishable from a defined result of false when the comparison is not true.
In other words if x or y are nullable value types that support <, then:
- x < y returns:
- true if x and y are both not null and x.Value < y.Value
- false if x and y are both not null and x.Value >= y.Value
- alsofalse if either x or y are null
It’s that second false that is problematic. Because with any of the relationship operators, we can assume that if < returns false, then >= should return true! But not so with Nullable<T> wrapped value types, so the assumption above, which works for non-nullable value types won’t work with Nullable<T> wrapped value types:
1 : if (x < y) 2 : {
3 : // x is less than y
4:
}
5 : else if (x > y) 6 : {
7 : // x is greater than y
8:
}
9 : else 10 : {
11 : // x and y must be equal by definition, right???
12:
}
So, again be aware that if you are using the <, <=, >, or >= relational comparison operators between Nullable<T> wrapped value types, you will always get a false if one of the values is null regardless of the value of the other. Protect yourself by checking the HasValue property first or comparing against null using == or != operators which do work as expected against null.
Or, you can choose to use the Nullable.Compare<T>() method, which considers null to be less than any other value, if that is where you consider null to be ranked in the ordering of your type.
Summary
When using performing arithmetic or comparison operators directly against a Nullable<T> wrapped value type, make sure you know exactly what will happen if one of the operands is null!
Better yet, either compare the operands to null or query the HasValue property to make sure a value exists before you attempt to compare it or involve it in arithmetic.
Alternatively, you can use the Compare<T>() method in the Nullable static class to compare nullable types, with the understanding that null is considered less than any valid value (even the minimum negative values).
Print | posted on Thursday, July 14, 2011 7:54 PM | Filed Under [ My BlogC#Software.NETLittle Pitfalls ]