Introduction to Nullability in C#

The .NET type system, or the Common Type System (CTS), categorizes types into two groups. The first is value types, where a value represents an instance itself, and any assignment of that value makes a new copy of the instance. Types such as int, float, boolean, or System.DateTime are examples of value types. The second is reference types, where a value is a reference to an instance allocated in the managed heap. Types such as string or object are examples.

As Java allows to assign null to a class type variable, C# also allows assigning null to a reference type variable.

void ToUpperAndPrint(string s) {
  s = s.ToUpper();
  Console.WriteLine(s);
}

string s1 = "Hello";
ToUpperAndPrint(s1); // HELLO

string s2 = null; // Ok, string is a reference type
ToUpperAndPrint(s2); // NullReferenceException in ToUpperAndPrint

This requires the programmer to make a separate logic for checking whether a value is null or not as below, and that increases the chance of unintentional null-related errors.

void ToUpperAndPrint(string s) {
  if (s == null)
    return;
  s = s.ToUpper();
  Console.WriteLine(s);
}

To solve this problem, C# 8.0 introduced null-state analysis, which determines whether a reference type value can be null or not at compile time and emits warnings or errors if there are any nullability mismatches. As shown below, if the compiler encounters #nullable enable at the top of a file, it performs nullability checks for that file.

#nullable enable

void ToUpperAndPrint(string s) {
  s = s.ToUpper();
  Console.WriteLine(s);
}

string s1 = "Hello";
ToUpperAndPrint(s1); // HELLO

string s2 = null; // Warning: nullability mismatch; s2 is not nullable
ToUpperAndPrint(s2); // NullReferenceException in ToUpperAndPrint

string? s3 = null; // Ok, s3 is nullable
ToUpperAndPrint(s3); // nullability mismatch; s3 is nullable

Thanks to null-state analysis, by postfixing ? to explicitly specify that the variable can hold null, we can reduce the bug due to NullReferenceException dramatically.

From C# 10 (.NET 6), this feature is enabled by default, even without #nullable enable.

The internals

The null-state analysis is based on NullableAttribute and NullableContextAttribute, which are the members of System.Runtime.CompileServices. However, they are not defined in System.Private.CoreLib.dll (or mscorlib.dll from the .NET Framework) where the basic .NET types are implemented, but they are generated as internal classes by the compiler, hence not accessible from C# code directly. (For a detailed description, please refer here.)

For example, the following code with a class User with a nullable property;

namespace MyService;
public class User {
    public string Name { get; set; }
    public User? Partner { get; set; }
}

actually interpreted as below:

namespace System.Runtime.CompilerServices {
  internal class NullableAttribute : Attribute {
    public readonly byte[] NullableFlags;
    public NullableAttribute(byte b) { NullableFlags = new byte[1] { b }; }
    public NullableAttribute(byte[] bytes) { NullableFlags = bytes; }
  }
  internal class NullableContextAttribute : Attribute {
    public readonly byte Flag;
    public NullableContextAttribute(byte b) { Flag = b; }
  }
}

namespace MyService {
  public class User {
    public string Name { get; set; }

    [System.Runtime.CompilerServices.Nullable(2)]
    private User _partner;
    [System.Runtime.CompilerServices.Nullable(2)]
    public User Partner {
        [System.Runtime.CompilerServices.NullableContext(2)]
        get => _partner;
        [System.Runtime.CompilerServices.NullableContext(2)]
        set => _partner = value;
    }
  }
}

Note that both NullableAttribute and NullableContextAttribute are internal, which makes them inaccessible both from the library where they’re included and from other assemblies referencing the library.

You may wonder what that constant 2 means in the code. As you see above, NullableAttribute contains an array of bytes and NullableContextAttribute have a byte. Each byte can be one of 0, 1, and 2, where