C# 2.0 Nullable Types






3.82/5 (21 votes)
Oct 3, 2005
10 min read

106184
General instructions on the use of nullable types in C#.
Introduction
What is null?
The term null is an interesting programming concept in that it does not mean the same as zero or blank; rather, it often implies missing or otherwise undefined data and because of this, is frequently used as a flag in this scenario. Nullability refers to the ability of a data type to accept and appropriately handle NULL values.
Approaches to nullability
Since the dawn of programming, there existed a need to support nullability for value types; yet most general purpose programming languages have historically provided little or no support in this area. The obvious scenario is in the instance where a program interacts with a database. Because databases typically provide definable nullability on all types situations like the one highlighted below may occur:
//User object method
public object Age
{
get
{
SqlCommand cmd = new SqlCommand(
"select age from user where username = @UserName ", conn);
cmd.Parameters.AddWithValue("UserName", name);
return cmd.ExecuteScalar();
}
}
//consuming object
public int GetAge(string name)
{
//get user object from database
User user = GetUser(name);
int age = (int) user.Age; //this value might be null
return age;
The example above declares a GetAge()
method which takes a string name as parameter. The first instruction of this method retrieves a valid user based on the name provided by the name parameter. The second instruction calls the Age
property of the retrieved user object to return the given user’s current age. The Age
property performs a simple scalar operation to retrieve the current user’s age. As you can see, nullability presents an issue on both sides of the fence. Data publishers like the Age
property must maintain generic interfaces to allow support for null
s or handle null
values and pass back symbolic representations in such instances. On the consuming side, the situation highlighted above will leave the programmer with no way of telling whether the returned value from the database is null
. If it is, the program will raise an exception. One quick fix to the problem is to allow and trap the exception as follows:
public int GetAge(string name)
{
//get user object from database
User user = GetUser(name);
int age = 0;
try
{
age = (int)user.Age; //this value might be null
}
catch (Exception ex)
{
age = -1;
}
return age;
What we see in the above example is the use of a try
…catch
block to handle all instances where the Age
property might return a null
value.
Many approaches exist for handling null
s and value types without direct language support, but all have shortcomings. One approach is to use a distinguished value like -1 to indicate a null
; unfortunately the strategy only works when an unused value can be identified. Hence the GetAge
method can be rewritten as follows to apply this strategy:
public int GetAge(string name)
{
//get user object from database
User user = GetUser(name);
object age = user.Age; //this value might be null
if(age != null)
return (int)age;
else
return - 1;
}
The above examples change the local variable age
from an int
type to a generic object type. Since the object type can handle null
values age
can be tested for null
and returned only if a value exists. If there is no value, a -1 is returned to the caller as a symbolic representation of null
.
One obvious problem with this approach is that the caller must have explicit knowledge of the meaning of -1; however, this can be easily mitigated by using a constant or enum
. The example below illustrates this concept:
public enum ReturnValues : int
{
NULL
}
public int GetAge(string name)
{
//get user object from database
User user = GetUser(name);
object age = user.Age; //this value might be null
if(age != null)
return (int)age;
else
return (int)ReturnValues.NULL;
}
In the example above, an enum
definition has been added which defines a NULL element; GetAge
has also been rewritten to use ReturnValues
enum
for returning null
s rather than an arbitrary number.
Another approach is to maintain boolean null indicators in separate fields or variables, but this will only work well in languages that provide facility to return multiple values. The example below illustrates two approaches to this strategy, one for a language which provides native ability to return multiple results from a method, another for a language which does not:
//language constructs for multiple return types
public int GetAge(string name, out bool isNull)
{
isNull = false;
//get user object from database
User user = GetUser(name);
object age = user.Age; //this value might be null
if (age != null)
return (int)age;
else
{
isNull = true;
return -1;
}
}
//no language constructs for
//multiple return types
public Hashtable GetAge(string name)
{
//declare hashtable and HasValue key
Hashtable retval = new Hashtable();
bool isNull = true;
retval.Add("HasValue",isNull);
//get user object from database
User user = GetUser(name);
object age = user.Age; //this value might be null
if (age != null)
{
retval.Add("value", (int)age);
return retval;
}
else
{
retval["HasValue"] = false;
return retval;
}
}
A third approach is to use a set of user-defined nullable types, but this only works for a closed set of types:
public class NullableType
{
public bool HasValue;
public object Value;
}
//no language constructs for multiple return types
public NullableType GetAge(string name)
{
//declare hashtable and HasValue key
NullableType retval;
retval.HasValue = true;
//get user object from database
User user = GetUser(name);
object age = user.Age; //this value might be null
if (age != null)
{
retval.Value = (int)age;
return retval;
}
else
{
retval.HasValue = false;
return retval;
}
}
Introducing C# nullables
With C# 2.0 comes the introduction of the nullable type as a complete and integrated solution for the nullability issue on all forms of value types. A C# nullable type is essentially a structure that combines a value of the underlying type with a boolean null indicator. Similar to our NullableType class in the previous section, 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. Nullable types also possess a default constructor which accepts as an argument, an instance of the underlying type of that nullable. Underlying types are discussed later on.
Nullable types are constructed using the ? type modifier. This token is placed immediately after the value type being defined as nullable. For instance, if we were trying to define a uint in its nullable form, we would apply the ? after making the token uint?. The type specified before the ? modifier in a nullable type is called the underlying type of the nullable type. Any value type can be an underlying type; the example below illustrates defining nullables for the 13 built in types:
char? nullChar = null;
byte? nullByte = null;
sbyte? nullsbyte = null;
short? nullShort = null;
ushort? nullushort = null;
int? nullInt = null;
uint? nullUint = null;
long? nullLong = null;
ulong? nullulong = null;
float? nullFloat = null;
double? nullDouble = null;
decimal? nullDecimal = null;
bool? nullBool = null;
A nullable indicator may also be applied to any struct
. The example below shows two declarations, one with a standard Point
struct
and the other with the nullable version of the Point
object:
Point point = new Point(10, 10);
Point? nullPoint = null;
nullPoint
can also be instantiated by invoking the default constructor discussed earlier in this section:
nullPoint = new Point?(point);
Using the nullable version of an enum
is no different. The example below instantiates to null
, a CommandType
enum
:
CommandType? ct = null;
In the previous section, we declared a GetAge()
method which took a string name as parameter. The first instruction of this method retrieves a valid user based on the name provided by the name parameter. The second instruction calls the Age
property of the retrieved user object to return the given user’s current age. We also provided the definition of the Age
property of the user
object which performed a simple scalar operation to retrieve the current user’s age. The examples are listed below for recap:
//User object method
public object Age
{
get
{
SqlCommand cmd = new SqlCommand(
"select age from user where username = @UserName ", conn);
cmd.Parameters.AddWithValue("UserName", name);
return cmd.ExecuteScalar();
}
}
//consuming object
public int GetAge(string name)
{
//get user object from database
User user = GetUser(name);
int age = (int) user.Age; //this value might be null
return age;
}
By utilizing nullable types, we can address the nullability requirements presented on both sides of the fence. Data publishers like the Age
property need no longer maintain generic interfaces to allow support for null
s or handle null
values and pass back symbolic representations in such instances. We can cast the results of ExecuteScalar
to an int?
and redefine the property signature to return int?
. On the consuming side, the programmer can now use the HasValue
property of int?
to determine whether the returning value from the database is indeed null
. The new version of the sample is listed below:
//part of User object
public int? Age
{
get
{
SqlCommand cmd = new SqlCommand(
"select age from user where username = @UserName ", conn);
cmd.Parameters.AddWithValue("UserName", name);
return (int?) cmd.ExecuteScalar();
}
}
//consumming object
public string GetAge(string name)
{
//get user object from database
User user = GetUser(name);
int? age = user.Age; //this value might be null
if (age.HasValue)
{
return age.Value.ToString();
}
else
{
return "No age found for user";
}
}
But wait there’s more: User defined nullable types
As if all this cool functionality wasn’t enough, you also have the ability to create your own user defined null
value types, and the beauty of it is that you do not have to do any extra work because any struct
or enum
you create will automatically have a nullable version of itself that can be utilized anywhere you need it. The example below illustrates this by creating a simple struct
Real
which encapsulates a 32 bit integer:
public struct Real
{
int internalVal;
public Real(int realNumber)
{
internalVal = realNumber ;
}
}
Once you have your user defined nullable type defined, you may use it as you would use any user defined type:
Real real = new Real(3);
You also have the ability to use the nullable version of your type, post fixed with the ?, just like the built-in nullables, but this cannot be accomplished by simply instantiating the nullable in the manner specified below:
Real? real = new Real?(3);
Attempting to compile the snippet below will produce a compiler error which states:
Cannot convert null to 'Real' because it is a value type
The rules do not change for your user-defined types. The nullable inferred from your struct
definition requires a standard Real
object as an argument to the constructor. Modifying the snippet above to accommodate this produces the following:
Real? real = new Real?(new Real(3));
User defined conversions may also be applied as with any struct
type. The code below adds an implicit conversion from int
to Real
.
public static implicit operator Real(int realNumber)
{
return new Real(realNumber);
}
Doing this gives you the approximate functional look and feel of the built-in nullables. Thus the real
instance can be initialized in the following ways:
Real? real = new Real?(new Real(null));
real = 9;
real? = null;
Default values
The default value of a nullable type is an instance for which the HasValue
property is false
and the Value
property is undefined. The default value is also known as the null value of the nullable type. An implicit conversion exists from the null
literal to any nullable type, and this conversion produces the null
value of the type.
Conversions
There are a number of conversions that can be applied to nullable types. From the previous section you have already seen that an implicit conversion exists from the null
literal to any nullable type, and this conversion produces the null
value of the type. This is illustrated with the simple statement int? x = null;
setting a nullable type to a given literal is also the result of an implicit conversion from the underlying literals type to that of the nullable type. For instance the statement:
int? x = 5;
is really an implicit conversion from int
to int?
.
There is also the concept of lifted and nullable conversions, which allow for predefined conversions that operate on non-nullable value types to also be applied to nullable forms of those types. The following code snippet will cause the number 104 to be displayed in the console window.
int character = (int)'h';
Console.WriteLine(character);
This of course is a general conversion from type char
to type int
. The same number is displayed if the snippet is rewritten to use the int?
nullable type.
int? character = (int?)'h';
Console.WriteLine(character);
Rewriting the sample to use char?
and int?
will still produce the same results:
char? c = 'h';
int? character = (int?)c;
Console.WriteLine(character);
The following diagram illustrates the conversion workflow specified above. The pattern it identifies can be applied across any lifted conversion between any two nullable types whose standard types have a predefined or user defined conversion.
Conversions between the nullable and standard version of a given value type vary based on direction. Converting from standard to nullable is always implicit whereas conversions from nullable back to standard is always explicit. The example below illustrates this:
int i = 10;
int? j = i; //implicit
i = (int)j; //explicit
Lifted operators
In the section titled user defined nullable types; we created an implicit operator that converted int
to Real
. We then utilized that operator on the nullable version of the Real
struct
with the instruction Real? real = 9;
. Lifted operators work essentially in the same manner. They permit the predefined and user defined operators that work on the standard value types to also work on the nullable versions of those types.
Null coalescence
C# 2.0 introduces a new operator called the null coalescing operator denoted by double question marks (??
). The null coalescing operator takes as arguments a nullable type to the left and the given nullable type’s underlying type on the right. If the instance is null
, the value on the right is returned otherwise the nullable instance value is returned. Examine the example below:
//nullable instance is set to null
Line 1: int x = 10;
Line 2: int? j = null;
Line 3: int answer1 = j ?? x;
Line 4: Console.WriteLine(answer1);
//nullable instance now has value
Line 5: j = 100;
Line 6: int answer2 = j ?? 10;
Line 7: Console.WriteLine(answer2);
The variable j
is of type nullable int
and is initially set to null
. The coalescing operator expression on line 3 will therefore evaluate to 10 since j
has no value. Hence ten is the value of answer1
and will be printed to the console window. Line 5 sets j
to 100, hence on line 6, the value will now evaluate to true
on j
. Consequently; answer will be set to 100 and 100 will be displayed on the console window.
The null coalescing operator is an easy way to test for null
and presents an alternative value should the nullable not have one. The important rule concerning this is that, in the case of nullables, the instance or value on the right must be of the same type as the underlying type of the instance on the right. However the null coalescing operating is not limited to nullables in its application.