You probably might know it already. But I am sure for most of the developers out there it comes as a surprise. It kind of makes sense because they have seen null being assigned to reference types only. So if Nullable<T> can be assigned a null, is there a special preferential treatment going on here. In this post we are going to discuss how this is handled by the framework and how we can use it to write better and more efficient codes. null represents the absence of a value, so assigning null to a Nullable value type just means that it has no value. In C# official text, Nullable type is defined as a value type being an extension of all other value types with a null value. For every non-nullable value type there is a corresponding nullable value type denoting the same set of values plus the value null. The underlying type T of a nullable type cannot be a nullable type, otherwise, all other value types are supported. This is a beginner - intermediate level post.
In one of our previous discussions we pondered on the idea of Passing by Value and reference. We discussed that here Value and Reference have nothing to do with value and reference types. In both cases, passing happens by value until we use the ref keyword specifically which causes passing by reference. This would cause a re-assignment to be reflected in the caller code for the actual variable passed by reference. There is no issue passing a value type by reference. This is similar to the idea that says reference types are always allocated on a heap and value types, on a stack. What about the members of a reference type which are themselves value types?
Assigning T to a Nullable<T> type
There is no difference between directly assigning a variable of underlying type or newing up the Nullable<T> directly. In the following code, we are declaring and initializing an Int32. We are then assigning it to a nullable using the short notation and longer one. In Microsoft's text, Wrapping is defined as an operation of creating a non-null instance of a value type with a given value. On the other hand, Un-Wrapping is defined as accessing the Value property of the nullable type instance.
Let's see how the code created by these two options. As you can see below compiler would generate the same IL code in both situations.
Assigning Nullable<T> to T
Nullable<T> cannot be directly assigned to its non-nullable counterpart. Static type checking shows the error at compile time.
An explicit cast to underlying non-nullable type should get you out of the compile time issue. But in the case when the variable has a null value assigned, it results in InvalidOperationException as follows:
As we discussed above, we have to be careful when we use the nullable types in the expressions. Let's see the following example where a Nullable<T> has been added to a non-Nullable. We have used different order in both expressions. Let's see how it affects our result. Here we are checking if the Nullable has been passed a null value, we are defaulting to zero in this case. Null-coelscing operator (??) can be used on a Nullable<T>.
Let's see how these methods are compiled. We can compare the IL generated by both methods. Here Calc seems to generate more IL than Calc2.
Now let's try to use these two methods which should be performing similar by passing the same arguments and compare the results. It seems that they print different messages to the console. But why? Shouldn't they be performing similarly with Add being a commutative operation.
Actually this is just a matter of operator precedence. Null-coalescing operator's ?? precedence has a lower priority than + . So the addition is happening first [0 + 2] and then the expression becomes as [ num2?? 2]. Now since num2 is not null, 3 is assigned to the result. Using a parenthesis in the expression should result in the same result. This is a very common issue which results in surprisingly wrong results. You can read more about C# operators and their precedence on msdn. Further information about null-coelscing operator can be found here.
It is a lot easier if we use GetValueOrDefault method provided by Nullable<T>. We can also specify a fixed default value instead of the default value of wrapped type using another overload of the same method.
There is a lot of confusion on msdn about conversion between the Nullable and underlying type. Some statements are just wrong e.g. this statement:
When the null value is converted to a non-nullable value type, it is implicitly converted to the zero value of the value type. This is certainly not the case, this results in InvalidOperationException. Additionally, this statement is also wrong: T? has a predefined narrowing conversion to T . We need an explicit cast here. We can give the writer benefit of doubt by thinking that it is explicit cast that she is talking about. But the example given is using an implicit cast, which is certainly not the case.
Boxing / Unboxing Nullable Types
Now that we have established that Nullable<T> are value types then, being value types, they need to go through the same boxing / unboxing when there is an assignment involved with a reference type. As a matter of fact, boxing a nullable type is actually boxing the underlying type. Following is the IL code generated for a code calling Console.Writeline with a Nullable<T> variable. Compiler decides to use the overload of the method with object type parameter. Please notice the special instruction for boxing in the IL here.
Similar IL instructions are generated when we make the same overload of Console.Writeline for an Int32 using explicit casting. Don't try this @ home ever.
Assigning an object to a Nullable type would result in unboxing. Since this is a narrowing conversion we need to use an explicit cast here.
C# language specification is clear in stating what is actually going on. Boxing a value of a nullable-type proceeds as follows: If the source value is null (HasValue property is false), the result is a null reference of the target type. Otherwise, the result is a reference to a boxed T produced by unwrapping and boxing the source value.
Nullable<T> and Null Reference Exception
What if we call Nullable<T>.HasValue? Since this hold a null value. Shouldn't this result in an error.
Basically assigning null to object type and Nullable<T> are not same. Let's look at the following code and generated IL. We can see that they don't result in similar IL. Assigning null to the former doesn't even actually assign null to the variable. How can the compiler do that as we have already established that Nullable<t> is not a reference type.
default Keyword & Nullable<T>
default keyword is generally used when we are writing algorithms based on Generics. This keyword is used to get the default value of a value or reference type used as type argument to a type or method based on Generics. For numeric types, it would be the default numeric value. Let's see what value would be used when we use this keyword with a Nullable<T> type.
The above code would initialize _taxRate with null, which is the default value of Nullable<T>. In the above code we can just use [ default(T) ] if we need to use the default value of the wrapped type. We can also use GetValueOrDefault() method as discussed above.
Operator Lifting
Operating lifting is the mechanism used when nullable types are used as operands. They can be applied with the same operators as the underlying types. If any operand is null, then the whole expression is evaluated to null. This is called null-propagation. Resharper is a great help here for obvious cases:
This excludes concat operation. Here we are using string.Format with a nullable holding null. Let's see how it is printed on the console:
In order to evaluate the expression, the candidate operators are lifted to be used from the wrapped type. Hence the term Lifting. The operands are converted to their non-nullable counterparts. After the evaluation of the expression, the result is converted back to nullable type. So Nullable<T> can use all the operators of the underlying type T, plus additional operators can also be applied including null-coelscing operator [??] as we discussed above.
Sunday, June 23, 2013
Nullable<T> is not a Reference Type
Labels:
.net framework,
boxing,
C#,
nullable,
struct,
unboxing,
value type
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment