Pattern matching and nullable reference types
Switch expressions with type, property, relational and list patterns, and what nullable reference types do and do not protect you from.
After this lesson you can
- Write a switch expression that combines type, property and relational patterns
- Explain why the order of switch arms matters and what happens when none matches
- Say what string? changes at compile time and what it cannot change at runtime
A switch expression picks a value by testing an input against patterns, top to bottom, and the first arm that matches wins:
string Classify(object? o) => o switch
{
null => "nothing",
Circle { Radius: <= 0 } => "degenerate circle",
Circle c => $"circle r={c.Radius}",
Rect { W: var w, H: var h } when w == h => $"square {w}",
Rect r => $"rect {r.W}x{r.H}",
int and (< 0 or > 100) => "int out of range",
int n => $"int {n}",
int[] and [var first, .., var last] => $"array {first}..{last}",
_ => "something else",
};
The patterns, in the order they appear:
null, a constant pattern.Circle { Radius: <= 0 }, a type pattern plus a property pattern containing a relational pattern.Circle c, a type test that also declares a typed variable.when, a guard for conditions a pattern cannot express.and,or,notcombine patterns.[var first, .., var last], a list pattern (C# 11):..matches any number of elements in between._, the discard, which matches everything.
public abstract record Shape;public record Circle(double Radius) : Shape;public record Rect(double W, double H) : Shape; public static class Solution{ static string Classify(object? o) => o switch { null => "nothing", Circle { Radius: <= 0 } => "degenerate circle", Circle c => $"circle r={c.Radius}", Rect { W: var w, H: var h } when w == h => $"square {w}", Rect r => $"rect {r.W}x{r.H}", int and (< 0 or > 100) => "int out of range", int n => $"int {n}", int[] and [var first, .., var last] => $"array {first}..{last}", _ => "something else", }; public static string Describe() => string.Join(" | ", new object?[] { null, new Circle(0), new Circle(2), new Rect(3, 3), new Rect(2, 5), 150, 7, new[] { 4, 5, 6 }, "hi", }.Select(Classify));}Order is semantic: Circle { Radius: <= 0 } must come before
Circle c, or the general arm would take every circle. The compiler
rejects an arm that an earlier arm already covers completely, but it
cannot know which of two overlapping arms you meant to win.
Pattern matching is also the tidy replacement for as plus a null
check: if (shape is Circle { Radius: > 10 } big) tests the type,
reads the property and binds big in one step.
Nullable reference types
With <Nullable>enable</Nullable> (the default in new projects),
string means "should never be null" and string? means "may be
null". The compiler tracks this through your code and warns when
you dereference a string? without a check, or pass a possible null
where string is expected.
It is a compile-time analysis only. Nothing is checked at runtime, and
the null-forgiving operator ! silences the warning outright:
#nullable enableusing System.Runtime.CompilerServices; public static class Solution{ static int LengthOf(string text) => text.Length; // declared non-nullable static string Kind(int n) => n switch { 0 => "zero", > 0 => "positive" }; public static string Describe() { var log = new List<string>(); try { LengthOf(null!); } catch (NullReferenceException) { log.Add("non-nullable string still got null: NullReferenceException"); } string? maybe = null; log.Add("maybe?.Length ?? -1 = " + (maybe?.Length ?? -1)); maybe ??= "set"; log.Add("after ??= : " + maybe); try { Kind(-5); } catch (SwitchExpressionException) { log.Add("no arm matched -5: SwitchExpressionException"); } return string.Join("; ", log); }}LengthOf(string text) was declared non-nullable, and still received
null and threw NullReferenceException. Public methods called from
code you do not control should still validate arguments, for example
with ArgumentNullException.ThrowIfNull(text). The same run shows
?. (stop and yield null if the left side is null), ?? (fallback)
and ??= (assign only if currently null).
Common mistakes
- A switch expression with no matching arm. It throws
SwitchExpressionExceptionat runtime (shown above forKind(-5)). The compiler warns (CS8509) when arms are not exhaustive; add a_arm or handle the missing case. - Putting the general arm first.
Circle caboveCircle { Radius: <= 0 }is a compile error; a generalwhen-guarded arm placed too early is not, and silently wins. - Treating
!as a fix.value!only tells the compiler to stop warning. If the value can be null, it will still throw later. - Believing parameters cannot be null. Callers compiled without the feature, reflection and deserialisers can all pass null.
Exercise · Pro
Price a parcel with one switch expression
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade