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.
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?.Cityisnullif anything in the chain is null.??null-coalescing:nickname ?? "anonymous".??=assigns only when null.is null/is not nullchecks (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
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 risksSwitchExpressionException. - Using
== nullon types that overload==.is nullcan'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
stringis non-null andstring?is nullable; heed the compiler's nullability warnings.- Use
?.,??and??=to handle null concisely; preferis nullchecks. - 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.