Explanation:
Support for nullability across all types, including value types, is essential when
interacting with databases, yet general purpose programming languages have historically
provided little or no support in this area. Many approaches exist for handling nulls
and value types without direct language support, but all have shortcomings. For
example, one approach is to use a “special” value (such as −1
for integers) to indicate null, but this only works when an unused value can be
identified. Another approach is to maintain boolean null indicators in separate
fields or variables, but this doesn’t work well for parameters and return
values. A third approach is to use a set of user-defined nullable types, but this
only works for a closed set of types. C#’s nullable types
solve this long standing problem by providing complete and integrated support for
nullable forms of all value types.
Nullable types are constructed using the
? type modifier. For example,
int? is the nullable form of the predefined type
int. A nullable type’s
underlying type must be a non-nullable value type.
A nullable type is a structure that combines a value of the underlying type with
a boolean null
ind
icator. An instance of a nullable type has two public read-only properties: HasValue, of
type bool,
and Value,
of the nullable type’s underlying type.
HasValue is true for a non-null instance and false for a null
instance. When HasValue
is true, the Value
property returns the contained value. When
HasValue is false, an attempt to access the
Value property throws an exception.
An implicit conversion exists from any non-nullable value type to a nullable form
of that type.
Furth
ermore, an implicit conversion exists from the
null literal to any nullable type. In the example
int? x = 123;
int? y = null;
if (x.HasValue)
Console.WriteLine(x.Value);
if (y.HasValue)
Console.WriteLine(y.Value);
the int value
123 and the
null literal
are implicitly converted to the nullable type
int?. The example outputs
123 for x,
but the second Console.WriteLine
isn’t executed because
y.HasValue is false.
Nullable conversions and lifted conversions
permit predefined and user-defined conversions that operate on non-nullable value
types to also be used with nullable forms of those types. Likewise,
lifted operators permit predefined and user-defined operators that work
for non-nullable value types also work for nullable forms of those types.
For every predefined conversion from a non-nullable value type
S to a non-nullable value type T, a predefined
nullable conversion automatically exists from
S? to T?.
This nullable conversion is a null propagating form of
the underlying conversion: It converts a null source value directly to a null target
value, but otherwise performs the underlying non-nullable conversion. Nullable conversions
are furthermore provided from
S to T?
and from S?
to T, the
latter as an explicit conversion that throws an exception if the source value is
null.
Some examples of nullable conversions are shown in the following.
int i = 123;
int? x = i;
// int --> int?
double? y = x;
// int? --> double?
int? z = (int?)y;
// double? --> int?
int j = (int)z;
// int? --> int
A user-defined conversion operator has a lifted form when the source and target
types are both non-nullable value types. A
? modifier is added to the the source and target types to create
the lifted form. Similar to predefined nullable conversions, lifted conversion operators
propagate nulls.
A non-comparison operator has a lifted form when the operand types and result type
are all non-nullable value types. For non-comparison operators, a
? modifier is added to each operand
type and the result type to create the lifted form. For example, the lifted form
of the predefined +
operator that takes two int
operands and returns an int
is an operator that takes two
int? operands and returns an
int?. Similar to lifted conversions, lifted non-comparison
operators are null propagating: If either operand of a lifted operator is null,
the result is null.
The following example uses a lifted
+ operator to add two
int? values:
int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x + y;
the assignment to z
effectively corresponds to:
int? z = x.HasValue && y.HasValue ? x.Value + y.Value : (int?)null;
Because an implicit conversion exists from a non-nullable value type to its nullable
form, a lifted operator is applicable when just one operand is of a nullable type.
The following example uses the same lifted
+ operator as the example above:
int? x = GetNullableInt();
int? y = x + 1;
If x is null,
y is assigned
null. Otherwise, y
is assigned the value of x
plus one.
The null propagating semantics of C#’s nullable conversions, lifted conversions,
and lifted non-comparison operators are very similar to the corresponding conversions
and operators in SQL. However, C#’s lifted comparison operators produce regular
boolean results rather than introducing SQL’s three-valued boolean logic.
A comparison operator (==,
!=, <,
>, <=,
>=) has a lifted form when the operand types are
both non-nullable value types and the result type is bool. The lifted form of a comparison
operator is formed by adding a ? modifier
to each operand type (but not to the result type). Lifted forms of the
== and != operators consider two null values
equal, and a null value unequal to a non-null value. Lifted forms of the
<, >,
<=, and >=
operators return false if one or both operands are null.
When one of the operands of the
== or !=
operator is the null
literal, the other operand may be of any nullable type regardless of whether the
underlying value type actually declares that operator. In cases where no operator
== or != implementation
is available, a check of the operand’s
HasValue property is substituted. The effect of this rule is that
statements such as
if (x == null) Console.WriteLine("x is null");
if (x != null) Console.WriteLine("x is non-null");
are permitted for an x
of any nullable type or reference type, thus providing a common way of performing
null checks for all types that can be null.
A new null coalescing operator,
??, is provided. The result of a
??
b is a
if a is non-null; otherwise, the result is
b. Intuitively,
b supplies the value to use when
a is null.
When a is
of a nullable type and b
is of a non-nullable type,
a ??
b returns
a non-nullable value, provided the appropriate implicit conversions exist between
the operand types. In the example
int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x ?? y;
int i = z ?? -1;
the type of x
??
y is int?, but the type of
z
?? -1
is int. The
latter operation is particularly convenient because it removes the
? from the type and at the same
time supplies the default value to use in the null case.
The null coalescing operator also works for reference types. The example
string s = GetStringValue();
Console.WriteLine(s ?? "Unspecified");
outputs the value of S, or outputs Unspecified if is null.
|