Reading — step 1 of 7
Learn
Modern C# (7.0 onward, with major additions in 8/9/10/11) has rich pattern matching — type tests, property patterns, positional patterns, switch expressions. The Microsoft docs cover this thoroughly; modern codebases lean on it heavily.
Type pattern (C# 7+)
object obj = "hello";
if (obj is string s)
{
Console.WriteLine(s.Length); // s is bound, narrowed to string
}
Replaces the verbose obj is string && (string)obj. Bind once, use many times.
switch statements with patterns
static string Describe(object o) => o switch
{
null => "null",
int n when n < 0 => "negative int",
int 0 => "zero",
int => "positive int",
string { Length: 0 } => "empty string",
string s => $"string of length {s.Length}",
_ => "something else"
};
Switch EXPRESSION (returns a value) vs switch STATEMENT (no value). Prefer expressions where possible.
Property patterns
record Point(int X, int Y);
static string Quadrant(Point p) => p switch
{
{ X: 0, Y: 0 } => "origin",
{ X: 0 } => "on Y axis",
{ Y: 0 } => "on X axis",
{ X: > 0, Y: > 0 } => "first quadrant",
{ X: < 0, Y: > 0 } => "second quadrant",
{ X: < 0, Y: < 0 } => "third quadrant",
_ => "fourth quadrant"
};
Match on properties without writing the type name. { X: 0 } matches any object with X == 0.
C# 9 added relational patterns (> 0, < 0) and logical patterns (and, or, not):
static string Grade(int score) => score switch
{
>= 90 and <= 100 => "A",
>= 80 and < 90 => "B",
>= 70 and < 80 => "C",
>= 60 and < 70 => "D",
< 60 and >= 0 => "F",
_ => "invalid"
};
Positional patterns (with records)
static string Describe(Point p) => p switch
{
(0, 0) => "origin",
(var x, var y) when x == y => "on diagonal",
(var x, _) when x > 0 => "right side",
_ => "other"
};
For types with Deconstruct (records have it auto-generated), positional pattern matching destructures.
List patterns (C# 11+)
static string DescribeArray(int[] arr) => arr switch
{
[] => "empty",
[var x] => $"single element {x}",
[var first, .. var rest, var last] => $"first {first}, last {last}, {rest.Length} in middle",
_ => "complicated"
};
[..] matches the rest. Powerful for list/array decomposition.
records — value-equality classes
public record User(string Name, int Age);
var a = new User("Ada", 36);
var b = new User("Ada", 36);
a == b // true — records compare by value
a.ToString(); // "User { Name = Ada, Age = 36 }"
var older = a with { Age = 37 }; // non-destructive update
record (C# 9+) auto-generates Equals, GetHashCode, ToString, Deconstruct, with expression. Like Kotlin data class or Swift's Equatable structs.
record struct (C# 10+) — value-type record. Best of both: cheap copy AND value semantics.
Exhaustiveness in switch expressions
C# warns (but doesn't error) when a switch expression isn't exhaustive. For sealed hierarchies or enums, it figures out the cases:
public enum Color { Red, Green, Blue }
static string ToHex(Color c) => c switch
{
Color.Red => "#ff0000",
Color.Green => "#00ff00",
Color.Blue => "#0000ff"
// No default — compiler warns about exhaustiveness
};
Add an enum value → compiler warns the switch is incomplete.
Common mistakes
- switch STATEMENT vs EXPRESSION — different syntax. Statement uses
case X:andbreak;. Expression usesX => ...with comma separators. - Forgetting the discard
_— pattern that matches anything is_, notdefault:. - Property pattern on null —
{ X: 0 }doesn't match null. Add explicitnull => .... - Records mutability — records are immutable by default. Use
withfor updates. - Positional pattern without Deconstruct — only works on records and types you've defined Deconstruct on.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…