Nullable Types

Reference types can represent a nonexistent value with a null reference. Value types, however, cannot ordinarily represent null values. For example:

string s = null;   // OK - reference type.
int i = null;      // Compile error - int cannot be null.

To represent null in a value type, you must use a special construct called a nullable type. A nullable type is denoted with a value type followed by the ? symbol:

int? i = null;                     // OK - Nullable Type
Console.WriteLine (i == null);     // True

Nullable<T> Struct

T? translates into System.Nullable<T>. Nullable<T> is a lightweight immutable structure, having only two fields, to represent Value and HasValue. The essence of System.Nullable<T> is very simple:

public struct Nullable<T> where T : struct
{
  public T Value {get;}
  public bool HasValue {get;}
  public T GetValueOrDefault();
  public T GetValueOrDefault (T defaultValue);
  ...
}

The code:

int? i = null;
Console.WriteLine (i == null);              // True

translates to:

Nullable<int> i = new Nullable<int>();
Console.WriteLine (! i.HasValue);           // True

Attempting to retrieve Value when HasValue is false throws an InvalidOperationException. GetValueOrDefault() returns Value if HasValue is true; otherwise, it returns new T() or a specified custom default value.

The default value of T? is null.

Nullable Conversions

The conversion from T to T? is implicit, and from T? to T is explicit. For example:

int? x = 5;        // implicit
int y = (int)x;    // explicit

The explicit cast is directly equivalent to calling the nullable object’s Value property. Hence, an InvalidOperationException is thrown if HasValue is false.

Boxing/Unboxing Nullable Values

When T? is boxed, the boxed value on the heap contains T, not T?. This optimization is possible because a boxed value is a reference type that can already express null.

C# also permits the unboxing of nullable types with the as operator. The result will be null if the cast fails:

object o = "string";
int? x = o as int?;
Console.WriteLine (x.HasValue);   // False

Operator Lifting

The Nullable<T> struct does not define operators such as <, >, or even ==. Despite this, the following code compiles and executes correctly:

int? x = 5;
int? y = 10;
bool b = x < y;      // true

This works because the compiler borrows or “lifts” the less-than operator from the underlying value type. Semantically, it translates the preceding comparison expression into this:

bool b = (x.HasValue && y.HasValue)
          ? (x.Value < y.Value)
          : false;

In other words, if both x and y have values, it compares via int’s less-than operator; otherwise, it returns false.

Operator lifting means you can implicitly use T’s operators on T?. You can define operators for T? in order to provide special-purpose null behavior, but in the vast majority of cases, it’s best to rely on the compiler automatically applying systematic nullable logic for you.

The compiler performs null logic differently depending on the category of operator.

Equality operators (==, !=)

Lifted equality operators handle nulls just like reference types do. This means two null values are equal:

Console.WriteLine (       null ==        null);  // True
Console.WriteLine ((bool?)null == (bool?)null);  // True

Further:

  • If exactly one operand is null, the operands are unequal.

  • If both operands are non-null, their Values are compared.

Relational operators (<, <=, >=, >)

The relational operators work on the principle that it is meaningless to compare null operands. This means comparing a null value to either a null or a non-null value returns false.

bool b = x < y;    // Translation:

bool b = (x == null || y == null)
  ? false
  : (x.Value < y.Value);

// b is false (assuming x is 5 and y is null)

All other operators (+, –, *, /, %, &, |, ^, <<, >>, +, ++, --, !, ~)

These operators return null when any of the operands are null. This pattern should be familiar to SQL users.

int? c = x + y;   // Translation:

int? c = (x == null || y == null)
         ? null
         : (int?) (x.Value + y.Value);

// c is null (assuming x is 5 and y is null)

An exception is when the & and | operators are applied to bool?, which we will discuss shortly.

Mixing nullable and non-nullable operators

You can mix and match nullable and non-nullable types (this works because there is an implicit conversion from T to T?):

int? a = null;
int b = 2;
int? c = a + b;   // c is null - equivalent to a + (int?)b

bool? with & and | Operators

When supplied operands of type bool?, the & and | operators treat null as an unknown value. So, null | true is true, because:

  • If the unknown value is false, the result would be true.

  • If the unknown value is true, the result would be true.

Similarly, null & false is false. This behavior would be familiar to SQL users. The following example enumerates other combinations:

bool? n = null, f = false, t = true;
Console.WriteLine (n | n);    // (null)
Console.WriteLine (n | f);    // (null)
Console.WriteLine (n | t);    // True
Console.WriteLine (n & n);    // (null)
Console.WriteLine (n & f);    // False
Console.WriteLine (n & t);    // (null)

Null Coalescing Operator

The ?? operator is the null coalescing operator, and it can be used with both nullable types and reference types. It says, “If the operand is non-null, give it to me; otherwise, give me a default value.” For example:

int? x = null;
int y = x ?? 5;        // y is 5

int? a = null, b = 1, c = 2;
Console.Write (a ?? b ?? c);  // 1 (first non-null value)

The ?? operator is equivalent to calling GetValueOrDefault with an explicit default value, except that the expression passed to GetValueOrDefault is never evaluated if the variable is not null.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.30.210