# Nullable Reference Types

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Advanced Development
Source: https://www.devtutorials.ai/learn/advanced-csharp/advanced-development/nullable-reference-types

> Make null visible to the compiler: nullable context, string vs string?, warnings, null checks, !, ??, ??=, nullable attributes, legacy code and avoiding NullReferenceException.

## Introduction to Nullable Reference Types

Think about a form with some boxes marked **required** and some marked **optional**.

Whoever reads the form knows straight away: the "Name" box always has a value, but the "Middle name" box might be empty. Nobody is surprised by an empty optional box.

Old C# code had no such marks. Any `string`, any `Customer`, any object reference could secretly be **`null`**. If you forgot to check, your program crashed with the most common .NET error of all:

```text
System.NullReferenceException: Object reference not set to an instance of an object.
```

**Nullable reference types** add labels to mark which types can be null and which cannot:

- `string` means **"never null"**.
- `string?` means **"might be null"**.

The compiler then checks your code and **warns you** whenever you might use a null by mistake, **before** the program runs.

**In simple words:** `?` says "this can be empty", and the compiler makes sure you check before you use it.

**Easy Way to Remember:** **no `?` = always has a value. `?` = check first.**

## What You Will Learn

In this lesson, you will learn:

1. How to turn on the nullable context.
2. The difference between `string` and `string?`.
3. What the nullability warnings mean.
4. Null checks the compiler understands.
5. The null-forgiving operator `!`.
6. The null-coalescing operators `??` and `??=`.
7. Nullable attributes such as `[NotNullWhen]`.
8. How to add nullability to older code.
9. Habits that prevent `NullReferenceException`.

All examples run in your browser. Nullable warnings are switched **off** by default in this editor, so each example starts with `#nullable enable`. Warnings show as **yellow squiggles**: hover over one to read it. A warning does not stop the program running.

## Start With a Working Example

Press **Run**, then hover over the squiggly line in the editor.

```csharp runnable title="The compiler warns before the crash"
#nullable enable
using System;

string name = "Asha";
string? nickname = FindNickname("Asha");     // might be null

Console.WriteLine($"Name length: {name.Length}");

// Warning CS8602: Dereference of a possibly null reference.
// The compiler knows nickname might be null here.
try
{
    Console.WriteLine($"Nickname length: {nickname.Length}");
}
catch (NullReferenceException)
{
    Console.WriteLine("Crash! The warning was right.");
}

// The fix: check first.
if (nickname is not null)
    Console.WriteLine($"Nickname length: {nickname.Length}");
else
    Console.WriteLine("No nickname");

static string? FindNickname(string name) => null;
```

The compiler marked `nickname.Length` with a warning **before** you ran anything. Running it proved the warning right. The checked version below it has no warning.

## Nullable Context

The **nullable context** is the switch that turns this feature on. When it is on, the compiler treats `string` as non-null and `string?` as nullable, and it gives warnings.

There are three ways to turn it on:

**1. For the whole project**, in the `.csproj` file. New projects created with `dotnet new` already have this:

```text
<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>
```

**2. For one file or part of a file**, with a directive:

```text
#nullable enable       // on from here
#nullable disable      // off from here
#nullable restore      // back to the project setting
```

**3. Warnings only, or annotations only**, for gradual migration:

```text
#nullable enable warnings      // warn, but treat old code as "unknown"
#nullable enable annotations   // allow ? marks, but no warnings yet
```

Turn nullable on for **every new project**. It costs nothing at runtime. It only changes what the compiler checks, and it catches a whole class of bugs for free.

## Nullable References

With the context on, every reference type has two forms:

| Write | Meaning | Can assign `null`? | Can use `.Member` straight away? |
|---|---|---|---|
| `string` | Never null | No (warning) | Yes |
| `string?` | Maybe null | Yes | No; check first (warning) |
| `Customer` | Never null | No (warning) | Yes |
| `Customer?` | Maybe null | Yes | No; check first (warning) |

This is the same `?` you already know from **nullable value types** such as `int?`. There is one big difference:

- `int?` is a **different runtime type**: `Nullable<int>`, with `.HasValue` and `.Value`.
- `string?` is **the same runtime type** as `string`. The `?` is only a note for the compiler.

```csharp runnable title="Nullable value types versus nullable reference types"
#nullable enable
using System;

int? age = null;              // Nullable<int>: a real wrapper type
Console.WriteLine($"age.HasValue: {age.HasValue}");
Console.WriteLine($"int? runtime type:    {typeof(int?).Name}");

string? city = null;          // still just a string at runtime
Console.WriteLine($"city is null: {city is null}");
Console.WriteLine($"string? runtime type: {typeof(string).Name}");

city = "Pune";
Console.WriteLine($"Now city has {city.Length} letters");   // no warning: the compiler saw the assignment
```

The last line has no warning. The compiler **tracks** the value: after `city = "Pune"`, it knows `city` is not null. This tracking is called **flow analysis**.

## Nullability Warnings

Warnings about nullability all have numbers starting with **CS86**. These are the main ones you will see:

| Warning | Means | Typical fix |
|---|---|---|
| **CS8600** | Converting a possible null to a non-nullable type | Make the variable `?`, or give a default with `??` |
| **CS8602** | Dereference of a possibly null reference | Check for null first |
| **CS8603** | Possible null reference return | Return a real value, or make the return type `?` |
| **CS8604** | Possible null argument for a non-nullable parameter | Check first, or make the parameter `?` |
| **CS8618** | Non-nullable property is not set by the constructor | Initialise it, add `required`, or make it `?` |
| **CS8625** | Cannot convert null literal to non-nullable type | Do not assign `null`, or make it `?` |

```csharp runnable title="Common warnings in one place"
#nullable enable
using System;

var customer = new Customer("Ravi");
Console.WriteLine(customer.Describe());

string? input = Console.ReadLine();        // null when there is no input
string city = input ?? "Unknown";          // fixed with ?? (no CS8600)
Console.WriteLine($"City: {city}");

class Customer
{
    public string Name { get; }
    public string? Email { get; set; }     // optional: fine to leave null
    public string Phone { get; set; } = "";// required-ish: given a default (no CS8618)

    public Customer(string name) => Name = name;

    public string Describe()
    {
        // Email.Length here would be CS8602, so we check:
        string email = Email ?? "(no email)";
        return $"{Name}, {email}, phone '{Phone}'";
    }
}
```

Nullable warnings are **only warnings**. The program still compiles, and a `null` can still sneak in at runtime from old code, JSON or reflection. Many teams turn the warnings into errors with `<WarningsAsErrors>nullable</WarningsAsErrors>`, so they cannot be ignored.

## Null Checks

The compiler's flow analysis understands normal null checks. After a check, it knows the variable is safe, and the warning goes away.

The compiler understands these null checks:

- `if (x is not null) { ... }`
- `if (x != null) { ... }`
- `if (x is null) return;` (an early exit)
- `if (x is { } value) { ... }` (a property pattern)
- `if (string.IsNullOrEmpty(x)) return;`
- `ArgumentNullException.ThrowIfNull(x);`

```csharp runnable title="Different null checks"
#nullable enable
using System;

PrintLength("Chennai");
PrintLength(null);
Greet(new User("Meera"));
Greet(null);

static void PrintLength(string? text)
{
    if (text is null)
    {
        Console.WriteLine("No text");
        return;                                   // early exit
    }
    Console.WriteLine($"{text} has {text.Length} letters");   // safe here
}

static void Greet(User? user)
{
    if (user is { Name: var name })               // pattern: not null, and grab Name
        Console.WriteLine($"Hello, {name}");
    else
        Console.WriteLine("Hello, guest");
}

record User(string Name);
```

### The Null-Conditional Operator `?.`

**`?.`** safely reads a member only if the object is not null. If the object **is** null, the entire expression becomes `null` instead of crashing.

```csharp runnable title="Safe member access with ?."
#nullable enable
using System;

Order? found = new Order(new Address("Kochi"));
Order? missing = null;

Console.WriteLine(found?.ShipTo?.City ?? "no city");
Console.WriteLine(missing?.ShipTo?.City ?? "no city");

int? length = missing?.ShipTo?.City.Length;    // int? because the chain might stop
Console.WriteLine($"Length: {length?.ToString() ?? "null"}");

record Address(string City);
record Order(Address? ShipTo);
```

### Checking Arguments

For public methods, **reject `null` arguments at the start**. Then the rest of the method knows these arguments are safe to use.

```text
public void Ship(Order order, string address)
{
    ArgumentNullException.ThrowIfNull(order);
    ArgumentException.ThrowIfNullOrWhiteSpace(address);
    ...
}
```

## Null-Forgiving Operator (!)

Sometimes **you** know a value is not null, but the compiler cannot prove it. The **null-forgiving operator `!`** tells the compiler: **"trust me, this is not null."** It removes the warning.

```csharp runnable title="Telling the compiler you know better"
#nullable enable
using System;
using System.Collections.Generic;

var prices = new Dictionary<string, decimal> { ["Pen"] = 10m, ["Notebook"] = 60m };

// Find returns string? because it might find nothing...
string? first = new List<string> { "Pen", "Notebook" }.Find(x => x.StartsWith("P"));

// ...but we just put "Pen" in the list, so we know it is found.
Console.WriteLine($"Found: {first!.ToUpper()}");

// A bad use of ! : this really can be null.
string? missing = new List<string> { "Pen" }.Find(x => x.StartsWith("Z"));
try
{
    Console.WriteLine(missing!.ToUpper());     // no warning, but still a crash
}
catch (NullReferenceException)
{
    Console.WriteLine("! only hides the warning. It does not make the value safe.");
}
```

`!` changes **nothing at runtime**. It only silences the compiler. If you are wrong, you still get a `NullReferenceException`. Every `!` in your code is a promise you must be sure of. Prefer a real check whenever you can.

`null!` is a common pattern for a field that a framework fills in later, such as an Entity Framework navigation property:

```text
public Customer Customer { get; set; } = null!;   // EF sets this when loading
```

## Null-Coalescing Operator (??)

**`??`** provides a **fallback value** when the left side is null.

```text
result = value ?? fallback;
```

- If `value` is not null, `result` is `value`.
- If `value` is null, `result` is `fallback`.

The result of `x ?? "default"` is always non-nullable. Use `??` to convert a `string?` into a `string`.

```csharp runnable title="Fallbacks with ??"
#nullable enable
using System;
using System.Collections.Generic;

var settings = new Dictionary<string, string?>
{
    ["Theme"] = "Dark",
    ["Language"] = null,
};

string theme = settings.GetValueOrDefault("Theme") ?? "Light";
string language = settings.GetValueOrDefault("Language") ?? "en-IN";
string font = settings.GetValueOrDefault("Font") ?? "Segoe UI";

Console.WriteLine($"{theme}, {language}, {font}");

// ?? can be chained: the first non-null wins
string? fromUser = null, fromCompany = null, fromSystem = "UTC+05:30";
Console.WriteLine($"Time zone: {fromUser ?? fromCompany ?? fromSystem ?? "UTC"}");

// ?? can throw when a value is required
try
{
    string apiKey = settings.GetValueOrDefault("ApiKey") ?? throw new InvalidOperationException("ApiKey is missing");
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
```

## Null-Coalescing Assignment (??=)

**`??=`** assigns a value **only if the variable is currently null**.

```text
x ??= value;     // same as: if (x is null) x = value;
```

It is perfect for **lazy initialisation**: creating something the first time it is needed.

```csharp runnable title="Create on first use with ??="
#nullable enable
using System;
using System.Collections.Generic;

var cache = new PriceCache();
Console.WriteLine(cache.Get("Pen"));
Console.WriteLine(cache.Get("Notebook"));
Console.WriteLine(cache.Get("Pen"));

class PriceCache
{
    private Dictionary<string, decimal>? _prices;     // not loaded yet

    public decimal Get(string item)
    {
        _prices ??= Load();                             // loads only the first time
        return _prices[item];
    }

    private static Dictionary<string, decimal> Load()
    {
        Console.WriteLine("(loading prices)");
        return new() { ["Pen"] = 10m, ["Notebook"] = 60m };
    }
}
```

| Operator | Name | Does |
|---|---|---|
| `x?.Y` | Null-conditional | Reads `Y` only if `x` is not null; otherwise gives `null` |
| `x ?? y` | Null-coalescing | Gives `x`, or `y` if `x` is null |
| `x ??= y` | Null-coalescing assignment | Sets `x = y` only if `x` is null |
| `x!` | Null-forgiving | Tells the compiler `x` is not null; no runtime effect |

## Nullable Annotations

Sometimes `?` is not enough to describe how a method behaves. Think about `int.TryParse`-style methods: the output is null **when the method returns false**, and not null **when it returns true**.

**Nullable attributes** in `System.Diagnostics.CodeAnalysis` describe these rules, so the compiler understands your methods:

| Attribute | Put it on | Means |
|---|---|---|
| `[NotNullWhen(true)]` | An `out` or nullable parameter | Not null when the method returns `true` |
| `[MaybeNullWhen(false)]` | An `out` parameter | May be null when the method returns `false` |
| `[NotNull]` | A parameter | Will not be null after the method returns (it throws otherwise) |
| `[MemberNotNull(nameof(X))]` | A method | Field or property `X` is not null after this method |
| `[NotNullIfNotNull("input")]` | A return value | Not null if `input` was not null |
| `[DoesNotReturn]` | A method | Never returns normally (always throws) |

```csharp runnable title="Teach the compiler about your TryFind method"
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

var phoneBook = new PhoneBook();

if (phoneBook.TryFind("Asha", out string? number))
    Console.WriteLine($"Asha: {number.Length} digits, {number}");   // no warning: number is not null here
else
    Console.WriteLine("Asha not found");

if (!phoneBook.TryFind("Zoya", out string? other))
    Console.WriteLine($"Zoya not found (value is {other ?? "null"})");

class PhoneBook
{
    private readonly Dictionary<string, string> _numbers = new() { ["Asha"] = "9840012345" };

    public bool TryFind(string name, [NotNullWhen(true)] out string? number)
        => _numbers.TryGetValue(name, out number);
}
```

Without `[NotNullWhen(true)]`, `number.Length` inside the `if` would get a CS8602 warning, because `number` is declared `string?`. The attribute tells the compiler how the method behaves.

### Required Members

For properties that **must** be set when an object is created, the cleanest fix for warning CS8618 is the **`required`** modifier:

```csharp runnable title="required fixes CS8618 without fake defaults"
#nullable enable
using System;

var product = new Product { Name = "Desk Lamp", Sku = "DL-204" };
Console.WriteLine($"{product.Name} ({product.Sku}), note: {product.Note ?? "none"}");

// var bad = new Product { Name = "Lamp" };   // error CS9035: required member 'Sku' must be set

class Product
{
    public required string Name { get; init; }
    public required string Sku { get; init; }
    public string? Note { get; init; }
}
```

## Working with Legacy Code

Most real projects have old code written before nullable reference types. Turning the feature on for a big project at once can produce **thousands** of warnings. Migrate step by step instead.

### A Safe Migration Plan

1. **Turn on warnings for new files only.** Add `#nullable enable` at the top of each new file, or each file you touch.
2. **Start at the bottom.** Annotate small, shared classes first (models, helpers). Code that uses them gets better warnings for free.
3. **Annotate public APIs.** Decide for every parameter and return value: can this be null? Add `?` where yes.
4. **Fix warnings, do not hide them.** Use checks and `??` before reaching for `!`.
5. **Flip the project switch** to `<Nullable>enable</Nullable>` when most files are done.
6. **Make nullable warnings errors** to keep it clean.

### Oblivious Code

Code compiled with nullable **off** is called **oblivious**. The compiler does not know whether its types can be null or not, so you get **no warnings** when you use them. Treat values from oblivious code as **possibly null** and check them.

```csharp runnable title="Mixing old and new code in one file"
#nullable disable
static class LegacyLookup
{
    // Written before nullable reference types: may return null with no warning.
    public static string FindManager(string employee) => employee == "Asha" ? "Ravi" : null;
}

#nullable enable
static class Program
{
    static void Main()
    {
        foreach (string employee in new[] { "Asha", "Kiran" })
        {
            string? manager = LegacyLookup.FindManager(employee);   // treat old results as maybe-null
            Console.WriteLine($"{employee} reports to {manager ?? "nobody yet"}");
        }
    }
}
```

The `FindManager` method can return `null` but says `string`. The compiler cannot help you there, so the new code stores the result in a `string?` and checks it.

## Preventing Null Reference Exceptions

Nullable reference types catch most problems at compile time. A few habits close the rest of the gaps:

1. **Turn nullable on** in every project, and treat its warnings as errors.
2. **Prefer non-null designs.** Return an empty list, not `null`. Use a "null object" (like a `NullLogger`) instead of `null`.
3. **Validate at the edges.** Check arguments in public methods with `ArgumentNullException.ThrowIfNull`. Check data from JSON, databases and users as it enters your program.
4. **Use `required` and constructors** so objects are complete when created.
5. **Use `?.`, `??` and `??=`** for optional values instead of long `if` chains.
6. **Use `!` rarely**, and only with a comment that explains why it is safe.
7. **Use `TryXxx` patterns** with `[NotNullWhen]` for lookups that might fail.

```csharp runnable title="Return empty, not null"
#nullable enable
using System;
using System.Collections.Generic;

foreach (string customer in new[] { "Asha", "New Customer" })
{
    IReadOnlyList<string> orders = GetOrders(customer);
    Console.WriteLine($"{customer}: {orders.Count} order(s)");   // no null check needed
}

static IReadOnlyList<string> GetOrders(string customer) =>
    customer == "Asha"
        ? new[] { "ORD-1001", "ORD-1007" }
        : Array.Empty<string>();          // empty, never null
```

Callers never have to ask "is it null?" because the answer is always a list, maybe an empty one.

## Where Nullable Reference Types Are Used

- **All new .NET projects**, which enable `<Nullable>` by default.
- **The .NET libraries themselves**, which are fully annotated.
- **ASP.NET Core model binding and validation**, where non-nullable properties are treated as required.
- **Entity Framework Core**, which makes non-nullable string properties `NOT NULL` columns.
- **Library authors**, who publish annotations so their users get warnings too.

## Common mistakes

| Mistake | Symptom | Fix |
|---|---|---|
| Using a `string?` without checking | Warning CS8602, then `NullReferenceException` | Check with `is not null`, or use `?.` and `??` |
| `!` to hide a warning on a value that can be null | Crash with no warning | Replace `!` with a real check |
| Non-nullable property not initialised | Warning CS8618 | `required`, a constructor, or a default value |
| Returning `null` from a method typed `string` | Warning CS8603 | Return `string?`, or a real value |
| Expecting `string?` to have `.HasValue` | Compile error | That is only for `int?` and other value types |
| Turning nullable on for a huge project at once | Thousands of warnings | Migrate file by file with `#nullable enable` |
| Trusting a legacy library's `string` return | Unexpected `null` at runtime | Treat oblivious results as `string?` |
| Returning `null` for an empty list | Callers forget to check and crash | Return an empty collection |

## What to Remember

- Nullable reference types make "might be null" visible: `string` never null, `string?` maybe null.
- Turn them on with `<Nullable>enable</Nullable>` or `#nullable enable`.
- The compiler tracks values and warns with CS86xx codes before the program runs.
- Check with `is null`, `is not null`, patterns, `?.` or early returns.
- `!` only silences the compiler; it does not make a value safe.
- `??` supplies a fallback; `??=` assigns only when the value is null.
- Attributes such as `[NotNullWhen(true)]` describe tricky methods to the compiler.
- Migrate old code file by file, and treat oblivious values as possibly null.
- Prefer designs where null cannot happen: `required`, empty collections and null objects.

Next: Logging
