Learning Objectives
- Check a type and declare a variable in one step with the
ispattern - Use
switchwith type, property, tuple and list patterns - Add conditions with the
whenclause. - See how pattern matching improves readability and safety.
Key concepts
if (obj is string s)
{
Console.WriteLine(s.Length); //s has already been cast to string
}
The boilerplate of the previous style if (obj is string) { string s = (string)obj; ... } is gone. Can also be combined with is not, and, or.
if (n is int x and > 0) ...
if (c is 'a' or 'A') ...
Since it is an expression, it can be directly substituted into a variable. default is replaced by _ (discard).
string label = day switch
{
1 => "month", 2 => "angry", 3 => "number",
_ => "other",
};
string sound = animal switch
{
Dog d => $"{d.Name} Meow",
Cat c => $"{c.Name} Meow",
_ => "Quiet...",
};
string zone = point switch
{
{ X: 0, Y: 0 } => "Origin",
{ X: 0 } => "Above Y axis",
{ Y: 0 } => "Above X axis",
{ X: > 0, Y: > 0 } => "1Quadrant",
_ => "other",
};
Comparison operators (>, <=...) can also be used as is.
(int x, int y) p = (1, 2);
string s = (x, y) switch
{
(0, 0) => "Origin",
var (a, b) when a == b => "Diagonal",
_ => "other",
};
int[] arr = [1, 2, 3];
string s = arr switch
{
[] => "empty",
[var only] => $"only one: {only}",
[1, .., 3] => "Starts with 1 Ends with 3",
[_, _, _] => "exactly three",
[var first, .. var rest] => $"First {first}, the rest {rest.Length}",
};
.. is "Anything in between is OK", and var name is received as the Slice/Element variable.
string s = age switch
{
int a when a < 0 => "Invalid value",
int a when a < 20 => "Youth",
_ => "adult",
};
See as example
foreach (object item in items)
{
if (item is string s) Console.WriteLine($"String: {s}");
else if (item is int n and > 0) Console.WriteLine($"Positive integer: {n}");
else if (item is double d) Console.WriteLine($"Real number: {d}");
else Console.WriteLine($"Other: {item}");
}
Execution result
String (5 characters): hello
Positive integer: 42
Real number: 3.14
Other: True
String (5 characters): world
Note: Like is int n and > 0, type and value conditions can be combined to pack a lot of meaning into one line.
static string Describe(Animal a) => a switch
{
Dog d => $"{d.Name}: Meow",
Cat c => $"{c.Name}: Meow",
Bird b => $"{b.Name}: Chirp",
_ => "Unknown animal",
};
Execution result
Baduki: Meow
Butterfly: Meow
Tweet: Tweet
Jjong: Meow
Note: Compare with the old polymorphism virtual Sound(). Pattern matching is cleaner when adding fewer animal types and branching logic is gathered in one place.
string zone = p switch
{
{ X: 0, Y: 0 } => "Origin",
{ X: 0 } => "Above Y axis",
{ Y: 0 } => "Above X axis",
{ X: > 0, Y: > 0 } => "1Quadrant",
{ X: < 0, Y: < 0 } => "3Quadrant",
_ => "Other Quadrants",
};
Execution result
(0, 0) → Origin
(0, 5) → On Y axis
(7, 0) → On X axis
(3, 4) → 1st quadrant
(-1, -2) → 3rd quadrant
Note: Branching of the expression switch picks the matching item first from top to bottom. Please put more specific conditions above.
string result = (p1, p2) switch
{
("rock", "scissors") => "P1 wins",
("paper", "rock") => "P1 wins",
("scissors", "paper") => "P1 wins",
var (a, b) when a == b => "draws",
_ => "P2 wins",
};
Execution result
rock vs scissors: P1 win
paper vs rock: P1 win
scissors vs scissors: draw
rock vs paper: P2 win
Memo: The rule table can be expressed as code. Attach variable decomposition conditions to var (a, b) when ....
string desc = arr switch
{
[] => "empty",
[var only] => $"One element ({only})",
[1, .., 3] => "Starts with 1 Ends with 3",
[_, _, _] => "exactly three",
[var first, .. var rest] => $"First {first}, the rest {rest.Length}",
};
Execution result
[] → Empty
[42] → One element (42)
[1,2,3] → Start with 1 End with 3
[1,99,3] → Start with 1 End with 3
[1,2,3,4,5] → Start with 1 End with 3 End
[9,8,7] → Exactly three
Note: Since [1, .., 3] is above [_, _, _], [1,2,3] is also caught in the first branch. Always be conscious of your priorities.
Common Mistakes
- Do not cover all paths in the
switchexpression — Ifdefault(_) is not present,MatchFailureExceptionis possible. The compiler also warns. - Code where a more general pattern is placed on top, so the more specific pattern below cannot be reached.
- Omitting null check in property pattern —
obj switch { { Prop: ... } => ... }does not match if obj is null. If that's your intention, OK, otherwise branch separately. - By overusing the
whenclause, the branch actually becomesif/else if, which reduces readability — in that case, it is better to just useif. - Use the list pattern for large collections — the pattern indexes by length, so it is better suited to Array/List than
IEnumerable.
Cleanup
ispattern = type check variable declaration additional condition- The
switchexpression returns a value, so thevar x = obj switch { ... };form is natural. - When combining the Type/Attribute/Tuple/List pattern, the branch logic draws the data shape as is.
- branch order has meaning — more specific patterns above
- The
whensection is the last resort. If you use too much,ifis better for readability.
Try it yourself
cd src/IsPattern && dotnet run
cd ../SwitchType && dotnet run
cd ../PropertyPattern && dotnet run
cd ../TuplePattern && dotnet run
cd ../ListPattern && dotnet run