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); ... }
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 steals 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
nonnull, 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 nonnull 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.