次の方法で共有


C# 2.0 Nullable Types

Introduction

The designers of C#2 have added the concept of nullable types to deal with a weakness of value types versus reference types. It is then essential to have properly assimilated these two notions.

Value types and null value paradigm

A reference is null when it does not reference any object. This is the default value taken by all references. You just need to have a glance at the code of any application to notice that developers commonly take advantage of null references. In general, the use of a null reference allows communicating some information:

  • A method which must return a reference to an object returns a null reference to indicate that the requested object cannot be found. This relieves developers from having to implement a binary error code.

  • When you encounter a method which accepts an argument of a reference type which can be null, this means that this argument is generally optional.

  • A field of a reference type which is null can be used to indicate that the object is being initialized, updated or even deleted and that it does not have a valid state.

The notion of nullness is also commonly used within relational databases to indicate that a value in a record is not assigned. This notion can also be used to designate an optional attribute in an XML element.

Amongst the several differences between the value and reference types, we can take a look at the fact that the notion of nullness doesn't exist for value type. This generally causes several problems. For example, how to interpret a null integer value (or not yet assigned) retrieved from a database or from an XML document? Multiple solutions exist but none are fully satisfying:

  • If the whole range of integer values is not usable, we create a convention. For example, a null integer value is represented by an integer equal to 0 or 1. The several disadvantages to this approach are evident: constraint to maintain everywhere in the code, possibility of changes in the range of values taken by this field...

  • We create a wrapper structure containing two fields, an integer and a boolean, that is set to false, means that we have a null value. Here, we must manage an additional structure for value type and an additional state for each value.

  • We create a wrapper class containing an integer field. Here, the disadvantage is that in addition to having to maintain a new class, we add a significant burden to the garbage collector by creating several small objects on the heap.

  • We use boxing, for example by casting our integer value into a reference of type object. In addition to not being type-safe, this solution also has the disadvantage of overloading the garbage collector.

To deal with this recurring problem, the designers of C#2/.NET2 decided to provide the concept of nullable types.

The System.Nullable<T> structure

The 2005 .NET framework offers the System.Nullable<T> generic structure which is defined as follows:

public

struct System.Nullable<T>
{
public Nullable(T value);
public static explicit operator T( T? value );
public static implicit operator T?( T value );
public bool HasValue { get; }
public T Value { get; }
public override bool Equals( object other );
public override int GetHashCode();
public T GetValueOrDefault();
public T GetValueOrDefault( T defaultValue );
public override string ToString();
}

This structure responds well to the problem of null values when the T type parameter takes the form of a value type such as int. Here is a little example which illustrates the use of this structure. The first version of Fct() uses the nullness of the string type while the second version uses the nullness of an instance of the Nullable<int> structure:

class

Foo
{
static string Fct( string s )
{
if ( s == null )
return null;
return s + s;
}
static System.Nullable<int> Fct( System.Nullable<int> ni )
{
if ( !ni.HasValue )
return ni;
return (System.Nullable<int>) ( ni.Value + ni.Value );
}
}

Evolution of the C# syntax: Nullable<T> and the null keyword

The C# syntax allows you to assign and to compare null keyword to an instance of System.Nullable<T>:

Nullable

<int> ni = null;
System.Diagnostics.Debug.Assert(ni == null);

These two lines are equivalent to:

// Call the default ctor which internally set 'Hasvalue' to false.

Nullable<int> ni = new Nullable<int>();
System.Diagnostics.Debug.Assert(!nullable1.HasValue);

The use of the System.Nullable<T> structure is intuitive but can quickly become blurring. It is not obvious that these two programs are equivalent (the two pieces of generated IL code are equivalent):

class

Program
{
static void Main()
{
System.Nullable<int> ni1 = 3;
System.Nullable<int> ni2 = 3;
bool b = ( ni1 == ni2 );
System.Diagnostics.Debug.Assert( b );
System.Nullable<int> ni3 = ni1 + ni2;
ni1++;
}
}

using

System;
class Program
{
static void Main()
{
Nullable<int> ni1 = new Nullable<int>(3);
Nullable<int> ni2 = new Nullable<int>(3);
bool b = ( ni1.GetValueOrDefault() == ni2.GetValueOrDefault() ) && ( ni1.HasValue == ni2.HasValue );
System.Diagnostics.Debug.Assert( b );
Nullable<int> ni3 = new Nullable<int>();
if ( ni1.HasValue && ni2.HasValue )
ni3 = new Nullable<int>( ni1.GetValueOrDefault() + ni2.GetValueOrDefault () );
if ( ni1.HasValue )
ni1 = new Nullable<int>( ni1.GetValueOrDefault() + 1 );
}
}

Also it might seem strange that the ni++ instruction called on the ni variable which is supposed to be null does not cause an exception of type NullReferenceException.

Evolution of the C# syntax: equivalence between Nullable<T> and T?

In C#2, you can follow the name of a non-nullable value type T by a question mark. In this case, the C#2 compiler will replace all T? expressions by Nullable<T>. To simplify, you can imagine that this is a pre-processing which is done directly on the source code, a little like a pre-compiler. Hence, the following line...

int? i = null;

... is equivalent to:

Nullable

<int> i = null;

For example, the two following methods are rigorously equivalent:

class

Foo
{
static System.Nullable<int> Fct1( System.Nullable<int> ni )
{
if ( !ni.HasValue )
return ni;
return (System.Nullable<int>) ( ni.Value + ni.Value );
}
static int? Fct2( int? ni )
{
if (ni == null)
return ni;
return ni + ni;
}
}

In general, equivalent instances of a nullable type mix well together:

class

Program
{
static void Main()
{
int? ni1 = null;
int? ni2 = 9;
int? ni3 = ni1 + ni2; // OK, ni3 is null.
int? ni4 = ni1 + 3; // OK, ni4 is null.
int? ni5 = ni2 + 3; // OK, ni5 is equal to 12.
ni1++; // OK, ni1 stays null.
ni2++; // OK, ni2 is now equal to 1.
}
}

However, the compiler will prevent you from implicitly converting an object of a nullable type to its underlying type. In addition, it is dangerous to do such a conversion explicitly without previously testing the value since it might raise an exception of type InvalidOperationException.

class Program
{
static void Main()
{
int? ni1 = null;
int? ni2 = 9;
int i1 = ni1; // KO: Cannot implicitly convert
// type 'int?' to 'int.
int i2 = ni2; // KO: Cannot implicitly convert
// type 'int?' to 'int.
int i3 = ni1 + ni2; // KO: Cannot implicitly convert
// type 'int?' to 'int.
int i4 = ni1 + 6; // KO: Cannot implicitly convert
// type 'int?' to 'int.
// Compilation OK but an InvalidCastException is raised
// at runtime since ni1 is still null at this point.
int i5 = (int) ni1;
}
}

No special treatment for bool? in C# 2.0

Contrarily to what you may have seen in the beta version of C # 2.0, in the final version there is not special processing of the bool? type by the if, while and for keywords. Hence, this example will not compile:

class

Program
{
static void Main()
{
bool? b = null;
// Cannot implicitly convert type 'bool?' to bool.
if ( b ) { /*...*/ }
}
}

Nullables types and boxing/unboxing

During the conception of .NET 2.0, until the last moment, nullable types were only an artifact and did not impact the CLR. They only impacted the C# 2.0 compiler and the System.Nullable<T> structure. This did pose a problem since when an instance of a nullable type was boxed it could not be null. There was then an incoherency compared to the manipulation of reference types:

string s = null;
object os = s; // os is a null reference.
int? i = null;
object oi = i; // oi was not a null reference.

Under the pressure from the community, Microsoft engineers decided to fix this problem. Hence, the assert in the following program is verified:

class

Program
{
static void Main()
{
int? ni = null;
object o = ni; // boxing
System.Diagnostics.Debug.Assert( o == null );
}
}

From the point of view of the CLR, an instance of a boxed value type T can be null. If it is not null, the CLR does not store any information to know if it was originated from the T or Nullable<T> types. You can then unbox such an object into any of the two types as shows the following example. However, be aware that you cannot unbox a null value:

class Program
{
static void Main()
{
int i1 = 76;
object o1 = i1; // boxing of an int
int? ni1 =(int?)o1; // unboxing to an int?
System.Diagnostics.Debug.Assert( ni1 == 76 );
int? ni2 = 98;
object o2 = ni2; // boxing of an int?
int i2 = (int)o2; // unboxing to an int
System.Diagnostics.Debug.Assert( i2 == 98 );
int? ni3 = null;
object o3 = ni3; // boxing of a null int?
int i3 = (int)o3; // unboxing -> NullReferenceException raised!
}
}

Nullable structures and enumerations

The concept of nullable type can also be used on structures and enumerations. This can however lead to crippling compilation errors which are illustrated in the following example where the Nullable<MyStruct> structure does not support the members of MyStruct.

struct

Struct
{
public Struct(int i) { m_i = i; }
public int m_i;
public void Fct(){}
}
class Program
{
static void Main()
{
Struct? ns1 = null; // OK
Struct? ns2 = new Struct?(3); // KO: Cannot implicitly convert
// type 'int' to 'Struct'.
Struct? ns3 = new Struct?(); // OK: the default Struct.ctor() is called.
Struct? ns4 = new Struct(3); // OK
Struct? ns5 = new Struct(); // OK: the default Struct.ctor() is called.
ns4.m_i = 8; // KO: System.Nullable<Struct>' does not
// contain a definition for 'm_i'.
ns4.Fct(); // KO: System.Nullable<Struct>' does not
// contain a definition for 'Fct'.
}
}

However, if the need is there, the compiler will know how to use custom definitions of operators:

struct

Struct
{
public Struct( int i ) { m_i = i; }
public int m_i;
public static Struct operator +( Struct a, Struct b)
{
return new Struct( a.m_i + b.m_i );
}
}
class Program
{
static void Main()
{
Struct? ns1 = new Struct(3);
Struct? ns2 = new Struct(2);
Struct? ns3 = null;
Struct? ns4 = ns1 + ns2; // OK, ns4.m_i is equal to 5.
Struct? ns5 = ns1 + ns3; // OK, ns5 is null.
}
}

In regards to an instance of a nullable enumeration, be aware that you must explicitly get the underlying value to use it. For example:

using

System;
class Program
{
enum MyEnum { VAL1, VAL2 }
static void Main()
{
MyEnum? e = null;
if( e == null )
Console.WriteLine( "e is null" );
else
switch( e.Value )
{
//Here, we must be sure that e is not null.
case MyEnum.VAL1: Console.WriteLine("e is equal to VAL1");
break;
case MyEnum.VAL2: Console.WriteLine("e is equal to VAL2");
break;
}
}
}

Summary

This article is extracted from Practical .NET2 and C#2 by Patrick Smacchia. One of the very first books targets .NET 2.0 and C# 2.0.

 

Title Practical .NET2 and C#2
Author Patrick Smacchia
Publisher ParadoxalPress
Published January 2006
ISBN 0-9766132-2-0
Price USD 59.95 (ParadoxalPress price: USD 33.99)
Nb Pages 896
Website www.PracticalDOT.NET (Browse and download the 647 listings)

Comments