Nullable Reference Types
By the end of this lesson
Use the compiler's null analysis to prevent null reference exceptions.
The most common crash in C# programs is a null reference exception. You read a property, the reference holds nothing, and the program stops. The message tells you what happened and nowhere near enough about why.
For most of the language's life, the type system had no opinion on this. A parameter of type string might always hold text, or might hold nothing on Tuesdays. The type said string either way, so the compiler could not help and neither could the reader.
Nullable reference types change what the type says. A string means a value is expected to be there. A string? means it might be absent, on purpose. Once you have written that distinction down, the compiler can follow your code and warn you where the two are mixed up.
#nullable enable
public class Employee
{
// The type says: an employee always has a full name.
public string FullName { get; }
// The question mark says: this one is legitimately absent for some people.
public string? MiddleName { get; }
public Employee(string fullName, string? middleName)
{
FullName = fullName;
MiddleName = middleName;
}
}
public static class Badges
{
public static int LettersInName(Employee employee)
{
int full = employee.FullName.Length; // no warning: FullName is not nullable
int middle = employee.MiddleName.Length; // warning CS8602: possible null dereference
return full + middle;
}
public static int LettersInNameChecked(Employee employee)
{
// The compiler follows the check, so inside the if it stops warning.
if (employee.MiddleName is not null)
{
return employee.FullName.Length + employee.MiddleName.Length;
}
return employee.FullName.Length;
}
}- #nullable enable switches the analysis on for the rest of the file. In practice you set it once for the whole project instead — see the note at the end of this lesson.
- string and string? are the same type at run time. The question mark is an annotation: it records your intent so the compiler can check the code around it.
- CS8602 is the warning that earns its keep. The compiler is not claiming MiddleName is null — it is pointing out that you declared it as possibly null and then read a member from it without checking.
- The second method shows the analysis following control flow. Once an if has ruled null out, the warning disappears inside that branch. You do not annotate the check; the compiler works it out.
- is not null reads better than != null and cannot be affected by a custom equality operator, which makes it the safer habit.
The two annotations, and what each one asks of the reader:
| string | string? | |
|---|---|---|
| What you are telling readers | A value is always present here | The value may be missing, and that is expected |
| Assigning null | Warning | Allowed, no warning |
| Reading .Length without a check | No warning | Warning until you check |
| Enforced by the runtime | No | No — neither annotation exists at run time |
| Typical use | Most properties, parameters and return values | Optional data: a middle name, a cancellation date, a lookup that found nothing |
| What a warning is telling you | You may be putting nothing into a slot that promised something | You are using a value you already said might be missing |
public static class Payroll
{
// 1. The null-forgiving operator. The ! tells the compiler to stop warning.
// It generates no check, so a null still crashes on the next line.
public static string CodeOrCrash(Employee employee) => employee.TaxCode!;
// 2. Say what should happen when the value is absent.
public static string CodeOrDefault(Employee employee) => employee.TaxCode ?? "BR";
// 3. Fail on purpose, at the point of the problem, with a message that names it.
public static string CodeOrThrow(Employee employee) =>
employee.TaxCode ?? throw new InvalidOperationException(
$"Employee {employee.FullName} has no tax code, so payroll cannot be run.");
}- The ! is the null-forgiving operator. It means "I know something the analysis does not". Sometimes that is true. Often it means the warning was correct and got silenced.
- ?? is the null-coalescing operator: use the left value unless it is null, in which case use the right. Option 2 makes a decision instead of deferring one.
- Option 3 uses ?? with a throw on the right. The program still fails, but it fails where the missing data was noticed, with a message naming the employee. Compare that with a stack trace two layers away saying only that something was null.
- Of the three, option 1 is the only one that leaves the reader no better informed than before.
Summary
- The annotations record intent: string means a value is expected, string? means it may be absent
- The compiler follows your checks through the code and warns where a possibly-null value is used unguarded
- Nothing is enforced at run time, so keep real validation at the edges where outside data arrives
- ! silences the analysis without changing behaviour, which makes frequent use of it a signal to look closer
- Enabling the feature on old code produces many warnings, and each one marks an assumption nobody had written down
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Write a Customer class with a required Email and an optional AlternateEmail. Annotate both honestly. Then write a method that returns the best contact address available.
Make the compiler warning-free without using the ! operator anywhere.
Show solution
Email is string and AlternateEmail is string?, which states the rule in the type itself. The constructor check on Email is what makes the non-nullable annotation true rather than merely hopeful, because data can arrive from outside the analysis.
BestContact needs no if statement. ?? already means "the alternate unless it is missing", and because Email is non-nullable the expression as a whole cannot produce null — so the return type is string, not string?. The compiler works that out and stops asking.
There is a second defensible answer: return string? and let the caller decide what to do when there is no address. That is the better design when a customer might genuinely have no contact details. The point of annotating is that this decision now appears in the signature instead of being discovered at run time.
public class Customer
{
public string Email { get; }
public string? AlternateEmail { get; }
public Customer(string email, string? alternateEmail = null)
{
if (string.IsNullOrWhiteSpace(email))
{
throw new ArgumentException("A customer needs an email address.", nameof(email));
}
Email = email;
AlternateEmail = alternateEmail;
}
// No warning: the left side may be null, the right side never is.
public string BestContact => AlternateEmail ?? Email;
}Think about it
Think about it
Your team enables nullable reference types on a five-year-old codebase and the build reports 1,240 warnings. A colleague suggests clearing them in an afternoon by adding ! wherever the compiler complains.
The build would go green. What would the team have lost?
Show solution
Every ! is a claim that the value cannot be null, made by whoever was clearing warnings at speed rather than by whoever understood the code. Some of those claims will be wrong, and each wrong one is a crash that the compiler had already found and was told to ignore.
The team also loses the record. A warning is a question waiting to be answered. Once it is suppressed, nothing remains to show that the question was ever asked, and the next reader sees code that looks deliberately designed.
The green build is worth very little on its own. Nobody was going to read 1,240 warnings, but that argues for fixing them in batches with the code fresh in mind, or enabling the analysis file by file as work touches each area. Slow and partial beats fast and false.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.