Introduction to Nullables
By Bill Wagner
Nullable value types introduce some complications into the .NET type system. In its first release, C# and .NET did not support nullable value types. Only reference types were nullable. A core principle for a value type was that it always had valid contents; it was never null. A nullable value type changes those rules. The concept of nullable value types is simple: provide consistent semantics around the concept of a null, or missing, value. The first mention of nullable value types sums this up in the C# spec:
For each non-nullable value type T there is a corresponding null type T?, which can hold an additional value null. For instance, int? is a type that can hold any 32 bit integer or the value null.
The implementation of nullable value types is encompassed in the Nullable<T> structure. int? is a C# shorthand for Nullable<int>. In general, StructType? Is a shorthand for Nullable<StructType>. This small sample shows the most common actions for a nullable int:
int? a = default(int?); // a is the null value int? b = default(int); //b is the integer 0. int? c = 5; // c is the integer 5. bool aHasValue = a.HasValue; // false bool bHasValue = b.HasValue; // true; bool cHasValue = c.HasValue; // true int aValue = a.Value; // throws InvalidOperationException. int bValue = b.Value; // bValue is 0 int cValue = c.Value; // cValue is 5; int aValueOrDefault = a.GetValueOrDefault(); // returns 0 int bValueOrDefault = b.GetValueOrDefault(); // returns 0 int cValueOrDefault = c.GetValueOrDefault(); // return 5 int aSafeValue = a ?? 42; // aSafeValue is 42. int bSafeValue = b ?? 42; // bSafeValue is 0;
I’ve used a few C# language features that are less common and deserve some explanation. First, the expressions default(int?) and default(int). The default() expression returns the default value for a type. If the type is a reference type, that means the null value, converted to that specified type. For any value type, that means the value type containing the 0 bit pattern. For instance, default(int) returns 0. default(float) returns 0f. default(int?) returns an int? where HasValue is false. Value is also 0, but directly accessing it will throw an exception.
The next syntax shows the ?? operator, called the null coalescing operator. If the left operand is non-null, it returns the left operand. Otherwise, it returns the right operand. You can write the same operation with the conditional operator:
aSafeValue = a.HasValue ? a.Value : 42;
Or using an if statement:
if (a.HasValue) aSafeValue = a.Value; else aSafeValue = 42;
Also, there is an overload of GetValueOrDefault() that can serve the same purpose:
aSafeValue = a.GetValueOrDefault(42);
The syntax introduced by nullable types may seem unfamiliar at first. However, once you work with it a bit, and remember the core concept, most developers find nullables easy to work with. More developers get confused by the many conversions that involve nullable value types. Remember that Nullable<T> is supposed to represent all possible values for T, plus the null value. That statement means that conversions from a T to a Nullable<T> are natural, and implicit:
int a = 24; int? b = a;
However, that’s only the beginning. You remember that there are implicit casts that convert int to float, and int to double. Similar conversions exist from int to float? and from int to double?:
int a = 24; float? c = a; double? d = a;
I’ve used numeric types for these examples, but the rules apply to any value types, including those you create. If you define implicit conversions among value types, the same conversions work among the nullable types for those value types. Keep the core reason in mind: Nullable<T> must represent every possible value of T, plus the null value. It should always be natural to convert from a T to T?.
The rules for explicit conversions are consistent with those for implicit conversions. For two types, S and T, if an explicit conversion exists from S to T, explicit conversions exist from S to T?, and from S? to T?:
// must use an explicit cast here: int a = 24; int? b = 36; short? g = (short)a; short? h = (short?)b;
Notice that in the first case, I can cast a to a short. In the second case, I must cast b to a short?. The conversion from nullable<int> to short could throw an InvalidOperationException when b.HasValue is false, as shown earlier. Again, as with the implicit casts, these rules apply to any value types you create: If you define explicit casts on the value types you create, those same casts will be available on the nullable types as well:
The above conversion operators are important because they make equality tests using T and Nullable<T> obvious and simple code. For example:
int a = 5; int? b = 5; bool same = (a == b); // same is true;
Because there is an implicit conversion from int to int?, this is evaluated as follows:
Equality between nullables has simple rules so that Nullable<T> can defer most of the work to the implementation of T.
Note that the two values are equal if T.Equals() returns true for the two types.
These rules and the implicit conversions discussed earlier can be combined:
int a = 5; float? c = a; bool moreConversion = (a == c);
The value of moreConversion is true. The statement above applies the conversions already discussed, along with a standard numeric conversion. The compiler performs the following implicit conversions to evaluate the expression “a == c”:
There are two more behaviors that Nullable<T> has that helps create the experience that a Nullable<T> acts like a T (allowing for the null value). One of those is that the is operator can be used to determine if a Nullable<T> has a value:
int? b = 5; bool check = b is int; // check is true. b = default(int?); check = b is int; // check is false.
Notice that the is operator returns true if HasValue is true. Also, I didn’t add examples that converted ints to floats, or doubles. Those conversions are not allowed, and using the is operator won’t compile. You can’t check to see if int? is a float: that’s always known to be false at compile time.
Nullable types do fundamentally change some of the rules in the .NET type system. Taken individually, there are a lot of rules, and quite a few things to remember. I find them much, much, easier to remember if keep in mind the core principle: Nullable<T> should behave as closely to a T as possible. The only difference is that a Nullable<T> has one extra possible value: null.
All the rules on conversions and the general behavior of Nullable<T> are designed so that Nullable<T> should behave just like T, with extra features that handle that extra value. The more you keep that in mind, it’s easier to keep in mind how the conversions work. Here are my two quick keys: