C# and .NET path · The modern language · Topic 7 of 14

Nullable reference types

Not started

Before C# 8 every reference type variable could hold null, and nothing in the type said whether it was expected to. Nullable reference types add that information as annotations: in a nullable-enabled context, string means "never null" and string? means "might be null", and the compiler warns you when the two are mixed carelessly. The runtime is unchanged; this is entirely a compile-time analysis.

Why it matters: NullReferenceException is the most common .NET crash. Nullable annotations move most of them from production to compiler warnings.

  1. 1Annotations, not new types

    With nullable enabled, string means 'should never be null' and string? means 'may be null'. At run time both are the same string type.

  2. 2Flow analysis

    The compiler follows each variable through ifs, returns and assignments, and warns when you dereference one that might be null.

  3. 3Warnings only

    Nothing is checked at run time. Data from JSON, reflection or old libraries can still be null, so validate at the edges.

  4. 4On by default

    New projects since .NET 6 have <Nullable>enable</Nullable>. Treat the warnings as errors in new code.

1

How it works

The explanation, with examples you can run.

What the compiler checks

string? FindName(int id) => id == 1 ? "Ada" : null;string? name = FindName(2);Console.WriteLine(name.Length);

Building this with .NET 10 printed warning CS8602: Dereference of a possibly null reference, and running it threw NullReferenceException on line 3. The fix is to handle the null case where it can occur:

if (name is null) { Console.WriteLine("unknown"); return; }Console.WriteLine(name.Length);          // no warning: the compiler knows name is not null hereConsole.WriteLine(name?.Length ?? 0);    // or: null-conditional plus a default

The compiler tracks a null state ("not null" or "maybe null") for every variable through branches, pattern matches and assignments. Common warnings are CS8600 (converting a possible null to a non-nullable type), CS8602 (dereferencing a maybe-null value), CS8618 (a non-nullable property not set by the constructor) and CS8625 (passing null where it isn't allowed).

Telling the compiler what you know

Some APIs have contracts flow analysis can't see. Attributes in System.Diagnostics.CodeAnalysis describe them:

bool TryGet(string key, [NotNullWhen(true)] out Customer? customer);[return: NotNullIfNotNull(nameof(input))] string? Trim(string? input);[MemberNotNull(nameof(_conn))] void EnsureOpen();

After if (TryGet(key, out var c)), c is treated as not null inside the if. The required modifier (C# 11) is the clean answer to CS8618 for properties that callers must set in an object initializer.

The null-forgiving operator ! (as in name!.Length) suppresses a warning and generates nothing. Use it only when you know more than the compiler, and leave a comment saying why.

Where nulls still get in

Annotations are a promise, not a guarantee. Values can still be null at run time when they come from:

  • deserialisation: System.Text.Json can leave a non-nullable property null if the JSON omits it, unless the property is required or you validate;
  • libraries compiled without annotations, whose types are "oblivious" and produce no warnings either way;
  • default of a struct containing reference fields, and uninitialised array elements.

So check arguments at public boundaries (ArgumentNullException.ThrowIfNull(arg)), and let the annotations protect everything inside.

Turning it on

New projects contain <Nullable>enable</Nullable>. For an existing codebase, enable it file by file with #nullable enable, annotate from the leaves (models, utilities) upwards, and then add <WarningsAsErrors>nullable</WarningsAsErrors> so new code can't regress.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.With nullable enabled, what does `string? name` change at run time compared with `string name`?
  2. 2.What does the null-forgiving operator `name!` do?
  3. 3.This compiles with warning CS8602. What happens when it runs?
    string? FindName(int id) => id == 1 ? "Ada" : null;string? name = FindName(2);Console.WriteLine(name.Length);

0 of 3 right so far. Get all three to finish this topic.