Nullable reference types (C# reference)

Note

This article covers nullable reference types. You can also declare nullable value types.

Nullable reference types are available in code that's in a nullable aware context. Nullable reference types, the null static analysis warnings, and the null-forgiving operator are optional language features. All are turned off by default. A nullable context is controlled at the project level using build settings, or in code using pragmas.

Important

All project templates enable the nullable context for the project. Projects created with earlier templates don't include this element, and these features are off unless you enable them in the project file or use pragmas.

In a nullable aware context:

  • A variable of a reference type T must be initialized with non-null, and can never be assigned a value that might be null.
  • A variable of a reference type T? can be initialized with null or assigned null, but is required to be checked against null before dereferencing.
  • A variable m of type T? is considered to be non-null when you apply the null-forgiving operator, as in m!.

The compiler enforces the distinctions between a non-nullable reference type T and a nullable reference type T? using the preceding rules. A variable of type T and a variable of type T? are the same .NET type. The following example declares a non-nullable string and a nullable string, and then uses the null-forgiving operator to assign a value to a non-nullable string:

string notNull = "Hello";
string? nullable = default;
notNull = nullable!; // null forgiveness

The variables notNull and nullable are both represented by the String type. Because the non-nullable and nullable types are both stored as the same type, there are several locations where using a nullable reference type isn't allowed. In general, a nullable reference type can't be used as a base class or implemented interface. A nullable reference type can't be used in any object creation or type testing expression. A nullable reference type can't be the type of a member access expression. The following examples show these constructs:

public MyClass : System.Object? // not allowed
{
}

var nullEmpty = System.String?.Empty; // Not allowed
var maybeObject = new object?(); // Not allowed
try
{
    if (thing is string? nullableString) // not allowed
        Console.WriteLine(nullableString);
} catch (Exception? e) // Not Allowed
{
    Console.WriteLine("error");
}

Nullable references and static analysis

The examples in the previous section illustrate the nature of nullable reference types. Nullable reference types aren't new class types, but rather annotations on existing reference types. The compiler uses those annotations to help you find potential null reference errors in your code. There's no runtime difference between a non-nullable reference type and a nullable reference type. The compiler doesn't add any runtime checking for non-nullable reference types. The benefits are in the compile-time analysis. The compiler generates warnings that help you find and fix potential null errors in your code. You declare your intent, and the compiler warns you when your code violates that intent.

In a nullable enabled context, the compiler performs static analysis on variables of any reference type, both nullable and non-nullable. The compiler tracks the null-state of each reference variable as either not-null or maybe-null. The default state of a non-nullable reference is not-null. The default state of a nullable reference is maybe-null.

Non-nullable reference types should always be safe to dereference because their null-state is not-null. To enforce that rule, the compiler issues warnings if a non-nullable reference type isn't initialized to a non-null value. Local variables must be assigned where they're declared. Every field must be assigned a not-null value, in a field initializer or every constructor. The compiler issues warnings when a non-nullable reference is assigned to a reference whose state is maybe-null. Generally, a non-nullable reference is not-null and no warnings are issued when those variables are dereferenced.

Note

If you assign a maybe-null expression to a non-nullable reference type, the compiler generates a warning. The compiler then generates warnings for that variable until it's assigned to a not-null expression.

Nullable reference types can be initialized or assigned to null. Therefore, static analysis must determine that a variable is not-null before it's dereferenced. If a nullable reference is determined to be maybe-null, assigning to a non-nullable reference variable generates a compiler warning. The following class shows examples of these warnings:

public class ProductDescription
{
    private string shortDescription;
    private string? detailedDescription;

    public ProductDescription() // Warning! shortDescription not initialized.
    {
    }

    public ProductDescription(string productDescription) =>
        this.shortDescription = productDescription;

    public void SetDescriptions(string productDescription, string? details=null)
    {
        shortDescription = productDescription;
        detailedDescription = details;
    }

    public string GetDescription()
    {
        if (detailedDescription.Length == 0) // Warning! dereference possible null
        {
            return shortDescription;
        }
        else
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
    }

    public string FullDescription()
    {
        if (detailedDescription == null)
        {
            return shortDescription;
        }
        else if (detailedDescription.Length > 0) // OK, detailedDescription can't be null.
        {
            return $"{shortDescription}\n{detailedDescription}";
        }
        return shortDescription;
    }
}

The following snippet shows where the compiler emits warnings when using this class:

string shortDescription = default; // Warning! non-nullable set to null;
var product = new ProductDescription(shortDescription); // Warning! static analysis knows shortDescription maybe null.

string description = "widget";
var item = new ProductDescription(description);

item.SetDescriptions(description, "These widgets will do everything.");

The preceding examples demonstrate how compiler's static analysis determines the null-state of reference variables. The compiler applies language rules for null checks and assignments to inform its analysis. The compiler can't make assumptions about the semantics of methods or properties. If you call methods that perform null checks, the compiler can't know those methods affect a variable's null-state. There are attributes you can add to your APIs to inform the compiler about the semantics of arguments and return values. Many common APIs in the .NET libraries have these attributes. For example, the compiler correctly interprets IsNullOrEmpty as a null check. For more information about the attributes that apply to null-state static analysis, see the article on Nullable attributes.

Setting the nullable context

There are two ways to control the nullable context. At the project level, you can add the <Nullable>enable</Nullable> project setting. In a single C# source file, you can add the #nullable enable pragma to enable the nullable context. See the article on setting a nullable strategy. Before .NET 6, new projects use the default, <Nullable>disable</Nullable>. Beginning with .NET 6, new projects include the <Nullable>enable</Nullable> element in the project file.

C# language specification

For more information, see the following proposals for the C# language specification:

See also