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
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
.
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.
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
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.
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 Value
s are compared.
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)
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.
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)
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.