Why can assign “null” to nullable types

by Mark Zhou 26. January 2010 23:53

.NET Framework 2.0 introduces “nullable” variables, from its meaning, nullable types allow a type object instance to be assigned with value, or a null reference (null in C#, Nothing in Visual Basic). While referencing the nullable types, developers can check if the instance contains value (by invoke HasValue property) then get the underlying value (by invoke Value property). This mechanism builds a more safer way to interoperate with the types that is transferred between different systems, and reduce the NullReferenceException occurrence in action.

Nullable types are value-typed generic types, which means the underlying type of the nullable type is defined through the type parameter, furthermore, the type parameter can just be value type. For example, you cannot define a nullable variable of underlying type “string”, because “string” is a reference type.

However you will find it looks very strange – though the type parameter is restricted to value types, we can still assign “null” to nullable types. This code may always work in C#:

int? nullableInt = null;

The value type variables cannot be assigned to null, if you are trying to do so on int type, you may get a compiler error: cannot convert source type “null” to target type “int”.

So why it is working on nullable types?

To conclude this problem, let’s think over – is it reasonable to use implicit operator overloading to implement this? The answers is yes. Operator overloading can help to convert source type to target type implicitly (using assignment statement and assignment operator “=”) or explicitly (use explicitly conversion operator “()”), you need to overload implicit operators to allow a reference type to assign to a value type, you may need to programmatically create a new instance of the target type, doing something with source type, clone properties from source type then return the instance of target type. By using the implicit operator, you gain the ability to implicitly assign instance of any other type to your source type, for example, you can use the following code to initialize a Complex object if defined implicit operators:

Complex complexNumber = "4 + 6i";

But the nullable type doesn’t use implicit operators to do so. To prove this, let’s take a look at the source code of System.Nullable<T>.

Code Snippet
  1. using System;
  2.  
  3. [Serializable, StructLayout(LayoutKind.Sequential), TypeDependency("System.Collections.Generic.NullableEqualityComparer`1"), TypeDependency("System.Collections.Generic.NullableComparer`1")]
  4. public struct Nullable<T> where T : struct
  5. {
  6.     private bool hasValue;
  7.     internal T value;
  8.     [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
  9.     public Nullable(T value)
  10.     {
  11.         this.value = value;
  12.         this.hasValue = true;
  13.     }
  14.  
  15.     public bool HasValue
  16.     {
  17.         [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
  18.         get
  19.         {
  20.             return this.hasValue;
  21.         }
  22.     }
  23.     public T Value
  24.     {
  25.         [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
  26.         get
  27.         {
  28.             if (!this.HasValue)
  29.             {
  30.                 ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_NoValue);
  31.             }
  32.             return this.value;
  33.         }
  34.     }
  35.     [TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
  36.     public T GetValueOrDefault()
  37.     {
  38.         return this.value;
  39.     }
  40.  
  41.     public T GetValueOrDefault(T defaultValue)
  42.     {
  43.         if (!this.HasValue)
  44.         {
  45.             return defaultValue;
  46.         }
  47.         return this.value;
  48.     }
  49.  
  50.     public override bool Equals(object other)
  51.     {
  52.         if (!this.HasValue)
  53.         {
  54.             return (other == null);
  55.         }
  56.         if (other == null)
  57.         {
  58.             return false;
  59.         }
  60.         return this.value.Equals(other);
  61.     }
  62.  
  63.     public override int GetHashCode()
  64.     {
  65.         if (!this.HasValue)
  66.         {
  67.             return 0;
  68.         }
  69.         return this.value.GetHashCode();
  70.     }
  71.  
  72.     public override string ToString()
  73.     {
  74.         if (!this.HasValue)
  75.         {
  76.             return "";
  77.         }
  78.         return this.value.ToString();
  79.     }
  80.  
  81.     public static implicit operator T?(T value)
  82.     {
  83.         return new T?(value);
  84.     }
  85.  
  86.     public static explicit operator T(T? value)
  87.     {
  88.         return value.Value;
  89.     }
  90. }

As you see, the implicit operator T and explicit operator T doesn’t handle the null particularly. So how does C# compiler translate the null reference to a nullable object? Let’s see the IL code for this statement.

The testing program code is showing below.

Code Snippet - Program.cs
  1. class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             int? testNullableInt = null;
  6.  
  7.             string testString = null;
  8.  
  9.             Console.WriteLine("Int: {0}, String: {1}", testNullableInt, testString);
  10.         }
  11.     }

The IL code for this program is here:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 3
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<int32> testNullableInt,
        [1] string testString)
    L_0000: nop 
    L_0001: ldloca.s testNullableInt
    L_0003: initobj [mscorlib]System.Nullable`1<int32>
    L_0009: ldnull 
    L_000a: stloc.1 
    L_000b: ldstr "Int: {0}, String: {1}"
    L_0010: ldloc.0 
    L_0011: box [mscorlib]System.Nullable`1<int32>
    L_0016: ldloc.1 
    L_0017: call void [mscorlib]System.Console::WriteLine(string, object, object)
    L_001c: nop 
    L_001d: ret 
}

See Line “L_0001” and “L_0003”, IL loads the address of the local variable at index 0 (testNullableInt) on to the evaluation stack, then calls the “initobj” instruction. “initobj” instruction initializes an object with a null reference or an appropriate default value of the specific primitive type. Comparing with line “L_0009” and “L_000a”, IL loads a null object on the top of the evaluation stack and store this to the local variable of index 1 (testString). You may now clear enough – the IL treats the null reference assignment to the nullable object as particular, use “initobj” instruction to initialize the nullable object. The testString is a reference type object, thus the IL loads a “null” object and can directly assign to testString.

Conclusion: Null reference assignment to a nullable type will be treated separately during compilation, different introductions will be used in different context of the null reference assignment statements.

Tags:

Comments (1) -

Dixin
Dixin People's Republic of China
1/28/2010 11:07:25 AM #

The null assignment is a syntactical sugar.

The last IL is euqal to:
Nullable<int> testNullableInt = new Nullable<int>();
string testString = null;

Reply

Add comment




  Country flag
biuquote
  • Comment
  • Preview
Loading


Translate This Page

About Mark

Mark is a developer who works for building base class libraries and tools for developers.

Mark's Awards

Microsoft Community Contributor

Month List

Who visit this site

Recent Comments

Comment RSS