C# Nullable Types and Pattern Matching

Prevent null reference errors with nullable reference types and null operators, and write expressive conditions with type, property, relational, list and positional patterns in is and switch expressions.

Lesson 23 of 43Advanced18 min read

The most common crash in C# programs has always been the NullReferenceException: code tries to use an object, but the variable holds null. The inventor of null references famously called them his "billion-dollar mistake". Modern C# fights back with nullable reference types: the compiler tracks which variables might be null and warns you before the program runs whenever you use one without checking.

With this feature turned on, which it is by default in every new .NET project, string means "never null" and string? means "might be null". C# also has compact operators for handling nulls: ?. to call a member only when the object exists, ?? to supply a default, and ??= to assign only when a variable is null.

The second half of this lesson is pattern matching, a family of features that let is and switch test the shape of data: its type, its property values, ranges, and even the contents of a list. Patterns turn long chains of if statements into short, readable rules, and they work hand in hand with null checks.

Nullable reference types

C#
string name = "Ada";        // must never be null
string? nickname = null;    // allowed to be null

Console.WriteLine(name.Length);
Console.WriteLine(nickname.Length);   // warning CS8602: possible null dereference
  • Nullable checking is switched on with <Nullable>enable</Nullable> in the project file, the default in new projects.
  • The warnings are about intent: they don't change how the program runs, but each one points at a possible crash.
  • After a check such as if (nickname is not null), the compiler knows the variable is safe inside the block.
  • The null-forgiving operator ! (nickname!.Length) tells the compiler "trust me, it's not null". Use it rarely; if you're wrong, the program still crashes.
  • Value types use the same ?: int? age = null; with age.HasValue and age.GetValueOrDefault().

Null operators

OperatorExampleMeaning
?.user?.EmailNull if user is null, otherwise its Email
?[]orders?[0]Null-safe indexing
??name ?? "Guest"Use the right side when the left is null
??=cache ??= Load()Assign only if currently null
is null / is not nullif (order is null) return;Clear, reliable null checks

They chain: customer?.Address?.City ?? "Unknown" reads a nested value safely in one line.

Pattern matching

Patterns test values in is expressions and switch expressions:

C#
object item = 42;
if (item is int n && n > 10) { }                      // type pattern

if (order is { Status: "Paid", Total: > 100 }) { }    // property pattern

string size = weight switch                          // relational patterns
{
    < 1 => "Small",
    >= 1 and < 10 => "Medium",
    _ => "Large"
};

if (point is (0, 0)) { }                              // positional pattern
if (args is [var first, ..]) { }                      // list pattern
  • Type patterns check a type and give a typed variable.
  • Property patterns { Prop: pattern } check properties, and can nest.
  • Relational (<, >=) and logical (and, or, not) patterns combine conditions.
  • Positional patterns use deconstruction, which records support automatically.
  • List patterns match arrays and lists by their elements; .. matches any number of items.

Telling the compiler a value isn't null

The compiler follows your null checks within a method, but it can't see inside other methods unless they say what they guarantee. Many .NET methods already do. After if (!string.IsNullOrEmpty(s)), the compiler knows s isn't null, and after ArgumentNullException.ThrowIfNull(customer); it treats customer as non-null for the rest of the method.

Your own "try" methods can make the same promise with attributes from System.Diagnostics.CodeAnalysis:

C#
using System.Diagnostics.CodeAnalysis;

static bool TryFindUser(Dictionary<int, string> users, int id,
                        [NotNullWhen(true)] out string? name)
    => users.TryGetValue(id, out name);

if (TryFindUser(users, 1, out string? name))
    Console.WriteLine(name.Length);   // no warning: true means name is set

[NotNullWhen(true)] says "when this method returns true, the out value isn't null". Without it, the compiler warns about name.Length even inside the if, and people are tempted to silence it with !. Related attributes include [MaybeNullWhen(false)] and [NotNull]. They describe your code to the compiler; they don't add any checks at run time.

C# Advanced Syntax

Nullable and pattern syntax

string? s = null;
int len = s?.Length ?? 0;
s ??= "default";
if (s is not null) { }
int? maybe = null;   int value = maybe ?? -1;
if (shape is Circle { Radius: > 10 } c) { }
string r = n switch { < 0 => "neg", 0 => "zero", _ => "pos" };
if (p is (> 0, > 0)) { }
if (list is [1, 2, ..]) { }
if (list is [.., var last]) { }

C# Advanced Summary

  • With nullable reference types, string can't be null and string? can; the compiler warns about unsafe use.
  • ?. ?[] ?? and ??= handle nulls concisely; is null and is not null check for null.
  • The null-forgiving ! silences a warning but doesn't prevent crashes.
  • Type, property, relational and logical patterns test values in is and switch.
  • Positional and list patterns match deconstructed values and collection contents.

C# Advanced Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Handling missing data safely

C#
var customers = new List<Customer>
{
    new("Ada", new Address("Leeds", "LS1 4AP")),
    new("Ben", new Address(null, "M1 1AE")),
    new("Cy", null)
};

foreach (Customer c in customers)
{
    string city = c.Address?.City ?? "Unknown city";
    int postcodeLength = c.Address?.Postcode?.Length ?? 0;
    Console.WriteLine($"{c.Name}: {city} ({postcodeLength})");
}

string? cachedReport = null;
cachedReport ??= BuildReport();
cachedReport ??= BuildReport();
Console.WriteLine(cachedReport);

string BuildReport()
{
    Console.WriteLine("Building report...");
    return "Report ready";
}

record Address(string? City, string? Postcode);
record Customer(string Name, Address? Address);
Output
Ada: Leeds (7)
Ben: Unknown city (6)
Cy: Unknown city (0)
Building report...
Report ready
Line-by-line explanation
  1. Line 10?. stops at the first null, and ?? supplies a fallback.
  2. Line 11Chaining works through several possibly-null levels.
  3. Lines 16 to 17??= only assigns when the variable is null, so the report is built once.
  4. Lines 26 to 27The ? marks show exactly which values may be missing.
Example 2

Shipping rules with property patterns

C#
var orders = new[]
{
    new Order("UK", 120m, false),
    new Order("UK", 30m, false),
    new Order("FR", 80m, false),
    new Order("US", 45m, true),
    new Order("JP", 200m, false)
};

foreach (var o in orders)
{
    Console.WriteLine($"{o.Country} {o.Total,6}: {Shipping(o)}");
}

string Shipping(Order order) => order switch
{
    { IsMember: true } => "Free for members",
    { Country: "UK", Total: >= 50 } => "Free UK delivery",
    { Country: "UK" } => "3.99",
    { Country: "FR" or "DE" or "ES", Total: > 60 } => "5.99",
    { Country: "FR" or "DE" or "ES" } => "9.99",
    _ => "Quote at checkout"
};

record Order(string Country, decimal Total, bool IsMember);
Output
UK    120: Free UK delivery
UK     30: 3.99
FR     80: 5.99
US     45: Free for members
JP    200: Quote at checkout
Line-by-line explanation
  1. Lines 17 to 19Property patterns check one or more properties at once.
  2. Line 17Arms are tried from top to bottom, so members are handled first.
  3. Line 18Total: >= 50 combines a property pattern with a relational pattern.
  4. Line 20or matches any of several values for one property.
  5. Line 22The discard handles every remaining case.
Example 3

Type, positional and list patterns

C#
object[] values = { 42, "hello", 3.5, null!, new Point(0, 0), new Point(3, -1), new[] { 1, 2, 3 } };

foreach (object v in values)
{
    string description = v switch
    {
        null => "null",
        int n when n > 10 => $"big int {n}",
        int n => $"int {n}",
        string { Length: > 3 } s => $"long string '{s}'",
        double d => $"double {d}",
        Point(0, 0) => "the origin",
        Point(var x, < 0) => $"below the x axis at x={x}",
        int[] [1, .., var last] => $"array starting with 1, ending with {last}",
        _ => "something else"
    };
    Console.WriteLine(description);
}

record Point(int X, int Y);
Output
big int 42
long string 'hello'
double 3.5
null
the origin
below the x axis at x=3
array starting with 1, ending with 3
Line-by-line explanation
  1. Line 7The null pattern handles null before anything else.
  2. Line 8A type pattern with a when clause adds an extra condition.
  3. Line 10A type pattern and a property pattern combined.
  4. Line 12A positional pattern deconstructs the record and compares both values.
  5. Line 13var x captures a value while < 0 tests the other.
  6. Line 14A list pattern checks the first element and captures the last; .. skips the middle.

Common C# Advanced Mistakes

Silencing warnings with !

Don't
string city = customer.Address!.City!;       // the warning goes away, the crash doesn't
Do
string city = customer.Address?.City ?? "Unknown";

Comparing with == null on types that overload ==

Don't
if (order == null) { }                       // can call a custom == operator
Do
if (order is null) { }                       // always a true null check

Broad patterns before narrow ones

Don't
string label = n switch
{
    > 0 => "positive",
    > 100 => "large",                       // error CS8510: already handled by a previous arm
    _ => "other"
};
Do
string label = n switch
{
    > 100 => "large",
    > 0 => "positive",
    _ => "other"
};

C# Advanced in Real Projects

Null safety and patterns in .NET code

Since .NET 6, every new project has nullable reference types turned on, and the .NET libraries themselves are annotated, so the compiler knows that Dictionary.GetValueOrDefault can return null. ASP.NET Core model binding and Entity Framework use these annotations to decide which fields are required. Pattern-matching switch expressions are common in business rules, such as pricing, shipping and permissions, and in code that handles different message or event types.

C# Advanced FAQ

What does the question mark after a type mean in C#?

On a value type, int? creates a real nullable type, Nullable<int>, which can hold a number or null and has HasValue and Value. On a reference type with nullable checking enabled, string? is an annotation telling the compiler the variable may be null, so it warns you before you use it unsafely. In both cases it signals "this might be missing".

What is the difference between ?? and ?. in C#?

?. is the null-conditional operator: user?.Email gives null instead of throwing when user is null. ?? is the null-coalescing operator: name ?? "Guest" supplies a fallback when the left side is null. They're often used together, as in user?.Email ?? "no email".

Should I use is null or == null in C#?

Prefer is null and is not null. They always check for a real null reference, while == can be redefined by a type's own operator overload and give surprising results. Both compile to the same check for ordinary types, so is null costs nothing extra.

How do I fix warning CS8602 possible null dereference?

Check the value before using it, for example with if (x is null) return; or x?.Length ?? 0, provide a default with ??, or change the declaration to a non-nullable type if null should never happen. Use the ! operator only when you're certain the value can't be null and the compiler simply can't see why.

What is a list pattern in C#?

A list pattern, available since C# 11, matches an array or list by its elements, written in square brackets. nums is [1, 2, 3] checks the exact contents, args is [var first, ..] matches one or more items and captures the first, and .. stands for any number of items. They work in is expressions and switch expressions.

C# Advanced Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the declaration so the variable may hold null.

string middleName = null;
Predict the outputeasy

What does this print?

string? s = null;
Console.WriteLine(s?.Length ?? -1);
Multiple choicemedium

What does x ??= 10; do?

Predict the outputmedium

What does this print?

var p = new Pt(2, 5);
Console.WriteLine(p is { X: > 0, Y: > 4 });
record Pt(int X, int Y);
Multiple choicehard

Does the null-forgiving operator ! prevent a NullReferenceException?

C# Advanced Quiz

5 questions to check what you learned.

  1. What does string? mean with nullable reference types enabled?

    • An optional parameter
    • A string that can't be null
    • A nullable int
    • A string that may be null
  2. What does user?.Email return when user is null?

    • null
    • An empty string
    • It throws
    • "null"
  3. Which pattern checks property values?

    • Type pattern
    • Property pattern { Name: value }
    • List pattern
    • Discard pattern
  4. What does .. mean in a list pattern like [1, .., 9]?

    • A range of numbers
    • Exactly two elements
    • Any number of elements in between
    • An error
  5. Why is is null preferred over == null?

    • It can't be changed by a custom == operator, so it's always a true null check
    • It's faster to type
    • == null doesn't compile
    • It works only for strings