Nullable in C#
19 March 2022

All examples from this article are available on GitHub:
https://github.com/BigLittleEndian/NullableInCSharp


After switching to .NET 6 and getting some warnings related to dereferencing nullable types I got an idea to write a complete article about nullable in C#. If you are someone who stopped learning about nullable after C#2 and introduction of nullable value types, this is the right article for you. It will show you all features related to nullable types and references up to and including C#10.

Dereferencing a variable means to access one of its members using the . (dot) operator.

History

null is a concept existing in many programming languages. Value null indicates that the pointer or reference does not point or refer to a valid object. It was introduced in 1965 as a part of the ALGOL W language. Its creator Tony Hoare (also famous for his Quicksort algorithm) later coined the term for it as a “billion dollar mistake”.

Nullable Value Types in C#2

If you create an integer (value type) and never assign to it, it will have a default value of 0. Problem is that 0 could be a legitimate value representing something other than “never assigned” or “undefined”. So, people started to invent values that represent “undefined”. I call those values “magic values” since every organization, every person, every project has different representation of “undefined”. Therefore, introducing nullable value types in C# 2.0 was a step in the right direction. The code below is showing how the magic number could be replaced with nullable value type.

public static void MagicVsNull()
{
int magicNumber = -1; // -1 represents empty or undefined
int? nullableNumber = null; // Nullable<int>

// Check is number valid (provided)

if (magicNumber == -1) // Magic value of -1
{
Console.WriteLine("magicNumber is not provided");
}

if (!nullableNumber.HasValue) // Nullable<int> structure has a property HasValue
{
Console.WriteLine("nullableNumber is not provided");
}
}

Suffix “?” after value type is making it nullable. Its default value is null.

Any nullable value type is an instance of the generic System.Nullable<T> structure. Appending “?” to int will create System.Nullable<int> structure. This structure is also adding some utilities like HasValue helper property from example above.

When working with nullable types you always need to check for null before checking the value:

nullableNumber = 55;

if(nullableNumber.HasValue && nullableNumber > 40)
{
Console.WriteLine("nullableNumber is greater than 40");
}

There is a shorter way to check for null and value:

if(nullableNumber is > 50)
{
Console.WriteLine("nullableNumber is greater than 50");
}

Nullable Reference Types in C#8

Code Is Not Changing

Feature name is confusing since reference types are already nullable. What is introduced in C#8 is a way to express intent and tell compiler that some objects could be null, and some objects will never be null. Using that information compiler is doing static analysis at design-time to determine the null-state of a variable and with warnings helping us to avoid NullReferenceException at the runtime.

Most important thing to understand is that IL is not changed. Nullable reference types are just semantically like nullable value types. Appending “?” to reference type (like string) will not create some new type or structure as it does for value types.

(value type)      int?    => System.Nullable<int>
(reference type)  string? => string

If string is not changed after appending “?” what is actually added in C#8?

C# 8 introduced two new fine-grained controls for how the compiler interprets reference type variables:

  • Annotations - if enabled you can use suffix “?” and null forgiving operator “!” to express intention so compiler can generate warnings.
  • Warnings - if enabled the compiler performs all null analysis and emits warnings when code might dereference null.
By using nullable reference types you are not changing IL therefore you are not fixing or introducing bugs.

Nullable reference types are compile time check. You express intent, compiler uses it to produce warnings. That is all.

New Bits from Nov 2021

.NET 6 project templates are turning on those nullable reference types features by default. Also, C#10 has number of improvements to null state analysis. Therefore, you will get fewer false positive nullable warnings.

Did You Turn Them on and off?

You can turn features on/off both or separately. Also, you can specify features per project or per file. If you are enabling nullable references feature on a big project, you can do it in chunks by enabling it on a single file:

#nullable enable

As mentioned above you can turn only one feature. As per example below annotations will allow use of suffix “?” but without warnings:

#nullable disable warnings
#nullable enable annotations

To be honest I don’t see much practical use of tuning on only one feature.

How It Works

Let’s have an example showing warnings already in the constructor (all examples in this article assume that nullable reference feature is enabled):

//----------------------------------------------------------------------------
// Constructor is making sure that First and Last name are not null.
// Commenting first two lines will produce warning.
//----------------------------------------------------------------------------
public Student(string firstName, string lastName, string? middleName)
{
FirstName = firstName;
LastName = lastName;

// Commenting line below will not generate warning since MiddleName can be null
MiddleName = middleName;
}

All 3 string members are the same and if we don’t assign any value to them, they will be null. MiddleName has suffix “?”. That is not changing underlying variable. With “?” we only instructed compiler that MiddleName can be null at any point of time. Default state of a non-nullable reference variable is not-null so First and Last should never be null. Compiler gets this seriously and starts complaining right away if we don’t assign FirstName and LastName in the constructor. Compiler is OK if we don’t assign MiddleName or if we assign null to it.

Any implicitly typed local variables (declared using var) are nullable reference types.

Observing the next piece of code will explain why using nullable reference types is a good idea.

//---------------------------------------------------------------------------
// Compiler is firing warning:
// "'MiddleName' may be null here. Dereference of a possibly null reference"
// Code will crash if MiddleName is null
//---------------------------------------------------------------------------
public int GetTotalLettersInNameWrong()
{
return FirstName.Length + LastName.Length + MiddleName.Length;
}

Compiler is showing warning only for MiddleName: Dereference of a possibly null reference. And it is 100% correct. FirstName and LastName name are never null, and MiddleName.Length will throw exception if MiddleName is null. To be more precise, FirstName and LastName still can be null if they are set to null in some other piece of code. A warning will be issued for that piece of code, not here.

Correct code is below. Before using it, we need to check MiddleName for null. There is a simpler way to guard from dereferencing null by using null-coalescing operator ?? or null-conditional operator ?.. For the sake of learning we will use if statement.

//---------------------------------------------------------------------
// Code is safe. MiddleName is checked for null before dereferenced.
//---------------------------------------------------------------------
public int GetTotalLettersInName()
{
int length = FirstName.Length + LastName.Length;

if(MiddleName is not null)
{
length += MiddleName.Length;
}

return length;
}

Compiler did static analysis and determined that we will call MiddleName.Length only when MiddleName is not null, so no warning is fired.

Proper ways to check object for nullability (C#9+):
is not null
is null
Don’t use operators == or =!.

Note that string? doesn’t have HasValue property like int?. Reminder, nullable reference types are not changing IL.

You Can Still Write Bad Code

Even with nullable warnings turned on you can write bad code where compiler will not generate warning, but runtime will crash with null exception. Especially if you are working in multithreaded environment there is no guarantee that expression evaluated by if will evaluate same not-null value again within if block. Here is a simplified example:

//-----------------------------------------------------------------------
// Code is NOT safe. Once checked MiddleCount will become null.
//----------------------------------------------------------------------
public int? MiddleCount => MiddleName?.Length;

public int GetTotalLettersInNameNoWarning()
{
int length = FirstName.Length + LastName.Length;

if(MiddleCount.HasValue)
{
MiddleName = null; // MiddleCount becomes null after if guard
length += MiddleCount.Value;
}

return length;
}

Without guard if(MiddleCount.HasValue) compiler is generating a warning. Once we put guard compiler is happy. It is not smart enough to understand that inside if block MiddleCount will become null once we reset MiddleName. Without any warnings code will crush in runtime for anyone with middle name.

Pitfalls

Current null state analysis is not covering all possible problems. First example is array of strings (see below). No warning is fired and code will crash at runtime on array[5].Length. Second example is related to default structure. For member Name we are not getting warning like we did for student’s first and last name in previous example. Student was a class; Person is a structure. So, if we pass default structure to PrintPerson(), Name is null and without any warnings code will crash at runtime.

public class NullableReferencesPitfalls
{
public struct Person
{
public string Name; // For class you would get warning here
public int Age;
}

public static void Run()
{
// 1) Arrays - no warnings and will crash at runtime
string[] array = new string[10];
Console.WriteLine(array[5].Length);

// 2) Default struct will create person with Name = null, no warning
// Code will crash at runtime on Name.ToUpper()
PrintPerson(default);
}

public static void PrintPerson(Person person)
{
Console.WriteLine($"{person.Name.ToUpper()} - {person.Age}");
}
}

! (Null-Forgiving) Operator

Sometimes you want intentionally to pass or assign null, and you don’t want compiler to complain about it. Most probable use case for this is unit testing.

Let’s assume we want to test this method:

public void SetFirstName(string firstName)
{
FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
}

Our test wants to make sure that illegal parameter null will throw exception, but we don’t want to get warning. Passing null value or nullable variable with suffix “!” will make compiler trust you know what you are doing, and it will not fire warning for calling SetFirstName() with null:

[TestMethod]
public void TestNullForgivingOperator()
{
var studentParker = new Student("Peter", "Parker", "Benjamin");

Assert.ThrowsException<ArgumentNullException>(
() => studentParker.SetFirstName(null!));
}

This null-forgiving operator could be applied to literal or variable: null! or someVariable!. You can use this operator like: MiddleName!.Length in case compiler is complaining and you are 100% sure MiddleName is not null.

Attributes

You can use precondition attributes [AllowNull] and [DisallowNull] instead of appending “?” or leaving it out. The only difference is that you will not get a proper help from IntelliSense.

More useful are postcondition attributes. Those are like hints for null state analysis helping it to understand the semantics of your other methods (API or helpers). .NET guys already applied attribute to string.IsNullOrEmpty() so you can use it to guard dereferencing nullable strings. Examples will make it more clear.

Postcondition attributes are MaybeNull and NotNull. In helper method below we are using attribute to tell compiler that string passed as a parameter will not be null once method returns. Again, code could be shorter but for the sake of learning we use if statement.

//------------------------------------------------------------
// Postcondition attributes:
// 1) MaybeNull
// 2) NotNull
//
// Example using NotNull
//------------------------------------------------------------
public static void SetEmptyIfNull([NotNull] ref string? text)
{
if(text is null)
{
text = string.Empty;
}
}

Using this method in constructor below we are avoiding compiler warnings. Once you remove attribute from helper method warnings will appear in the constructor.

//------------------------------------------------------------------
// In this constructor compiler knows that SetEmptyIfNull() helper
// will make sure first and last name are not null.
// SetEmptyIfNull() is using postcondition NotNull attribute
//
// Example using NotNull postcondition attribute
//------------------------------------------------------------------
public Student(string? firstName, string? lastName)
{
SetEmptyIfNull(ref firstName);
SetEmptyIfNull(ref lastName);

FirstName = firstName;
LastName = lastName;

MiddleName = null;
}

Conditional postcondition attributes are MaybeNullWhen, NotNullWhen and NotNullIfNotNull. In example below, string passed as an argument is not null if function returns true.

//-----------------------------------------------------
// Conditional postcondition attributes:
// 1) MaybeNullWhen
// 2) NotNullWhen
// 3) NotNullIfNotNull
//
// Example using NotNullWhen
//-----------------------------------------------------
public static bool StringIsNotNull([NotNullWhen(true)] string? name) => name is not null;

Code below is without warnings since compiler static analysis uses attribute to understand that within if block MiddleName is never null.

//-----------------------------------------------------------------------------------
// Code is safe. Helper is checking MiddleName for null before dereferencing.
// Helper is using conditional postcondition attribute NotNullWhen to help analysis.
//
// Example using NotNullWhen conditional postcondition attribute
//-----------------------------------------------------------------------------------
public int GetTotalLettersInNameWithConditionalHelper()
{
int length = FirstName.Length + LastName.Length;

// Compiler knows that if StringIsNotNull returns true, MiddleName is not null
// StringIsNotNull helper uses NotNullWhen conditional postcondition attribute
if (StringIsNotNull(MiddleName))
{
length += MiddleName.Length;
}

return length;
}

Method and property helper methods attributes are MemberNotNull and MemberNotNullWhen. Best usage for this is making a constructor helper method to set some properties outside the constructor:

//------------------------------------------------------------------
// Method and property helper methods attributes:
// 1) MemberNotNull
// 2) MemberNotNullWhen (using bool value that method returns)
//
// Example using MemberNotNull
//------------------------------------------------------------------
[MemberNotNull(nameof(FirstName), nameof(LastName))]
public void SetFirstLastNameEmpty()
{
FirstName = LastName = string.Empty;
}

With MemberNotNull attribute we are telling caller (constructor) that this method is setting FirstName and LastName to not null so no warnings will be fired.

//--------------------------------------------------------------------------
// In default constructor compiler knows that SetFirstLastNameEmpty()
// helper method will set First and Last name since method is using
// MemberNotNull attribute
//
// Example using MemberNotNull method and property helper methods attribute
//--------------------------------------------------------------------------
public Student()
{
// MemberNotNull attribute is marking First and Last name as not null
SetFirstLastNameEmpty();
}

Unreachable code attributes are DoesNotReturn and DoesNotReturnIf. In example below, helper method is letting the compiler know that if argument is true, code will throw exception, therefore all code after calling this method will be unreachable.

//-------------------------------------------------------------
// Unreachable code attributes:
// 1) DoesNotReturn
// 2) DoesNotReturnIf
//
// Example using DoesNotReturnIf unreachable code attribute
//-------------------------------------------------------------
public static void FailIf([DoesNotReturnIf(true)] bool isNull)
{
if (isNull)
{
throw new InvalidOperationException();
}
}

In method below compiler did analysis and concluded that if MiddleName is null, FailIf() method will throw and MiddleName will never be dereferenced, so there is no reason to warn.

//--------------------------------------------------------------------
// No warning since FailIf will throw exception.
// Helper is notifying compiler that code below is unreachable if
// passed boolean is true (if MiddleName is null).
//
// Example using DoesNotReturnIf unreachable code attribute
//--------------------------------------------------------------------
public int GetTotalLettersInNameWithUnreachableCodeHelper()
{
int length = FirstName.Length + LastName.Length;

// If true method will throw and code below will be unreachable
FailIf(MiddleName is null);

return length + MiddleName.Length;
}

Generics

For generics we need to specify capabilities and expectations of a type parameter. With those we can also express nullability:

  • where T : class - Reference type. T must be a non-nullable reference type.
  • where T : class? - Reference type, either nullable or non-nullable.
  • where T : notnull - Non-nullable type. The argument can be a non-nullable reference type or a non-nullable value type.

I didn’t list <base class> & <interface name> with and without ? since the idea is the same.

Example below is showing notnull constraint. T can be value or reference type but as we will see in the usage later, they are not working the same. What is important to notice is that in Text getter only NullableValue must be guarded and checked for null. Value is of type T and constraint is making is sure that T is not nullable.

//------------------------------------------------------------------------------
// notnull constraint
//
// The type argument must be a non-nullable type. The argument can be:
// 1) non-nullable reference type in C# 8.0+
// 2) non-nullable value type
//------------------------------------------------------------------------------
public class NotNullableGeneric<T> where T: notnull
{
public T Value { get; set; }

// Since T is not nullable we can apply ? and create nullable:
public T? NullableValue { get; set; }

public NotNullableGeneric(T value, T? nullableValue)
{
Value = value;
NullableValue = nullableValue;
}

public string? Text
{
get
{
// We need null-conditional operator ?. only for NullableValue
// There is no need to guard Value due to constraint notnull
return Value.ToString() + // Not guarded
NullableValue?.ToString(); // Guarded with ?.
}
}
}

There is a different behaviour for value type and reference type (see example below). When T is string, constructor has parameters string and string?. When we make same generic from int, constructor is expecting int and int. Second parameter is not nullable! Not sure why but at least this is how it works in C#10 for now.

//
// notnull constraint example
//
Console.WriteLine("NotNullableGeneric:");

// Second parameter is string? so we can pass null
var stringExample = new NotNullableGeneric<string>("Luke", null);
Console.WriteLine("NullableGeneric<string>:");
Console.WriteLine($"Text: {stringExample.Text}");

// Both parameters are "int".
// Strange, for reference type above we got "string, string?" types.
var intExample = new NotNullableGeneric<int>(55, 44);
Console.WriteLine("NullableGeneric<int>:");
Console.WriteLine($"Text: {intExample.Text}");

// Line below will generate warning since T = stirng? is violating notnull constraint
var nullStringExample = new NotNullableGeneric<string?>(null, null);
Console.WriteLine($"Text: {nullStringExample.Text}"); // Code is crashing on null.ToString()

In the last example we are passing nullable string and that is against notnull constraint. Compiler will fire a warning. If we ignore this warning and pass null as a first parameter, Text getter will crash on not guarded Value.ToString().

You can play with class? constraint and you will notice that if T is string?, T? is same string? (second question mark is ignored). This relaxation of the rule is introduced in C# 9.0

Conclusion

When I decided to write about nullable my expectation was that it will be one of the shortest articles. At the end I needed to skip some stuff to keep article from being too long. I partially ignored nullable reference types when they appeared in C#8, but .NET 6 and nullability turned on by default triggered me to think more about it. It is a good feature.