C# Fundamentals: From Your First Program to Async and JSON · C# Basics: Types, Collections, Classes and Pattern Matching

Null Safety and Pattern Matching

Use nullable reference types, the ?. ?? and ??= operators, and C# patterns (type, property, relational, list) to write code that handles every case.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A message router. It accepts messages of different shapes (object values from untrusted input), uses pattern matching to classify them, and handles missing values without NullReferenceException.

The idea

Null safety. With <Nullable>enable</Nullable> (the default in new projects), a reference type such as string means not null and string? means may be null. The compiler warns when you might dereference a null. Tools:

  • ?. null-conditional: user?.Address?.City is null if anything in the chain is null.
  • ?? null-coalescing: nickname ?? "anonymous". ??= assigns only when null.
  • is null / is not null checks (preferred over == null).
  • ArgumentNullException.ThrowIfNull(x) validates arguments.
  • The ! operator tells the compiler "trust me, not null"; use it sparingly.

Pattern matching tests the shape of a value and extracts parts:

  • Type pattern: value is string text.
  • Property pattern: order is { Status: "paid", Total: > 100 }.
  • Relational and logical patterns: age is >= 18 and < 65, x is not null.
  • List patterns: [var first, .., var last], [], [1, 2].
  • Switch expressions combine all of these and must be exhaustive.

The code

Classifying untrusted input with patterns
object?[] inputs =
[
    "hello",
    42,
    -7,
    3.5,
    null,
    new Order("paid", 250m),
    new Order("paid", 40m),
    new Order("cancelled", 999m),
    new[] { 1, 2, 3, 4 },
    Array.Empty<int>(),
];

foreach (var input in inputs)
    Console.WriteLine($"{Show(input),-26} -> {Classify(input)}");

// ?. ?? ??= on nullable values
string? nickname = null;
Console.WriteLine(nickname?.ToUpper() ?? "no nickname");
nickname ??= "Ada";
Console.WriteLine(nickname.ToUpper());

static string Classify(object? value) => value switch
{
    null => "nothing",
    string { Length: 0 } => "empty string",
    string s => $"text of {s.Length} chars",
    int n and < 0 => "negative integer",
    int n => $"integer {n}",
    double d => $"decimal {d:F1}",
    Order { Status: "paid", Total: > 100 } => "large paid order",
    Order { Status: "cancelled" } => "cancelled order",
    Order o => $"order {o.Status}",
    int[] numbers => ClassifyArray(numbers),
    _ => "unknown",
};

// List patterns match arrays and lists by length and contents
static string ClassifyArray(int[] numbers) => numbers switch
{
    [] => "empty array",
    [var only] => $"array with one item: {only}",
    [var first, .., var last] => $"array from {first} to {last}",
};

static string Show(object? value) => value switch
{
    null => "null",
    int[] a => $"int[{a.Length}]",
    Order o => $"Order({o.Status}, {o.Total})",
    _ => value.ToString()!,
};

record Order(string Status, decimal Total);

Output

hello                      -> text of 5 chars
42                         -> integer 42
-7                         -> negative integer
3.5                        -> decimal 3.5
null                       -> nothing
Order(paid, 250)           -> large paid order
Order(paid, 40)            -> order paid
Order(cancelled, 999)      -> cancelled order
int[4]                     -> array from 1 to 4
int[0]                     -> empty array
no nickname
ADA

Run this on your computer — see “Get Started” for setup.

Switch arms are checked in order, so the specific cases (Total: > 100) must come before the general one (Order o). The compiler warns if an arm can never match.

Common mistakes

  • Silencing warnings with ! instead of handling null. Each ! is a bug waiting to happen.
  • Putting a general pattern before a specific one. The specific arm is never reached.
  • Forgetting the discard _ in a switch expression, which makes it non-exhaustive and risks SwitchExpressionException.
  • Using == null on types that overload ==. is null can't be overridden and is always correct.
  • Declaring non-nullable properties without initializing them. Make them required, initialize in the constructor, or make them nullable.

Key points

  • string is non-null and string? is nullable; heed the compiler's nullability warnings.
  • Use ?., ?? and ??= to handle null concisely; prefer is null checks.
  • Patterns (type, property, relational, list) test shape and extract values in one step.
  • Switch expressions must be exhaustive; order specific arms before general ones.

Further reading: Pattern matching.

Practice questions

What is the difference between `string` and `string?` when nullable reference types are enabled?

Answer

string promises never to be null, and the compiler warns if you assign or pass null. string? says null is allowed, and the compiler warns if you dereference it without checking.

Why does the order of arms in a switch expression matter?

Answer

Arms are tested top to bottom and the first match wins. A general arm placed before a specific one hides it, so the specific arm is unreachable (the compiler reports an error for subsumed patterns).

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?