How to Write Elegant Code With C# Switch Expressions
Learn C# switch expressions the easy way. Master pattern matching with type, relational, property, and list patterns using simple examples, diagrams, and tables.
Choosing the right counter at the railway station
Picture a busy railway station near your home. You walk in with a question. Maybe you want a ticket. Maybe you want to ask about a late train. Maybe you lost your bag.
There is a helpful guard at the entrance. You tell the guard your need, and the guard points you to the correct counter in one sentence. "Ticket? Counter 3." "Lost bag? Counter 7." "Late train? Ask the help desk."
The guard does not write a long essay. The guard looks at your situation, matches it to the right answer, and sends you off. One input, one clear answer.
A C# switch expression works just like that helpful guard. You give it one value. It looks at that value, matches it against a list of patterns, and hands you back one answer. Short, clean, and easy to read.
In this guide you will learn how switch expressions work, how they differ from the old switch statement, and how pattern matching makes them powerful. We will use simple examples you can try right away.
The old way: the switch statement
Before switch expressions, C# only had the switch statement. It works, but it is wordy. Let us look at a small example. We want to turn a day number into a day name.
string GetDayName(int day)
{
switch (day)
{
case 1:
return "Monday";
case 2:
return "Tuesday";
case 3:
return "Wednesday";
default:
return "Unknown";
}
}Look at all the noise. The word case repeats. The word return repeats. There are colons and a default. For such a simple job, that is a lot of typing.
The switch statement is a set of instructions. It does things. It does not naturally hand back a value. You have to add return or set a variable inside each case. That is where switch expressions help.
The new way: the switch expression
A switch expression says one thing: "Look at this value and give me back the matching result." Here is the same code as a switch expression.
string GetDayName(int day) => day switch
{
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
_ => "Unknown"
};Notice the differences:
- The value comes first, then the word
switch. - Each arm uses the arrow
=>instead ofcaseandbreak. - The underscore
_is the catch-all, likedefault. - The whole thing is one expression, so we assign it straight to the method.
It is shorter, calmer, and easier to read. Each line reads almost like English: "1 gives Monday."
How a switch expression runs
Steps
Input
You pass one value in
Check arms
Top to bottom, find first match
Guard check
If a when clause exists, test it
Return value
Hand back the matching result
The parts of a switch expression
Let us name the pieces so the rest of the guide is clear.
| Part | What it looks like | What it means |
|---|---|---|
| Input | day | The value you are testing |
| Keyword | switch | Starts the expression |
| Arm | 1 => "Monday" | One pattern and its result |
| Pattern | 1 | The thing to match against |
| Arrow | => | Separates pattern from result |
| Discard | _ => ... | Catches everything else |
A switch expression is just a list of arms wrapped in braces. Each arm is a pattern, an arrow, and a result. The first arm whose pattern matches wins.
Pattern matching: the real magic
The arms above used plain numbers like 1 and 2. Those are constant patterns. But switch expressions can match much smarter patterns. This is called pattern matching, and it is what makes switch expressions feel elegant.
Here are the main pattern types you will use.
| Pattern | Example | Use it to |
|---|---|---|
| Constant | 1, "Mon", true | Match an exact value |
| Type | int n, string s | Match the type of an object |
| Relational | > 90, < 0 | Compare with a number |
| Property | { Age: > 18 } | Look inside an object |
| List | [1, 2, 3] | Match the shape of a sequence |
| Logical | > 0 and < 10 | Join patterns with and, or, not |
| Var | var x | Grab the value into a name |
| Discard | _ | Match anything left over |
Let us see each one in friendly examples.
Relational patterns: comparing numbers
A relational pattern compares the input against a constant using <, >, <=, or >=. This is perfect for grading marks.
char GetGrade(int marks) => marks switch
{
>= 90 => 'A',
>= 75 => 'B',
>= 60 => 'C',
>= 40 => 'D',
_ => 'F'
};Read it from top to bottom. If marks are 95, the first arm >= 90 matches and you get 'A'. If marks are 70, the first two arms fail, but >= 60 matches and you get 'C'.
Order matters here, just like with a list of if-else checks. The first matching arm wins, so you put the most specific or highest checks first.
Logical patterns: joining conditions
You can combine patterns with the words and, or, and not. These are logical patterns. They let you write clear rules without nested if statements.
string DescribeNumber(int n) => n switch
{
< 0 => "negative",
0 => "zero",
> 0 and < 10 => "small",
>= 10 => "big",
_ => "unknown"
};The arm > 0 and < 10 matches numbers between 1 and 9. This reads almost like a sentence. Compare that with a tangle of if (n > 0 && n < 10) blocks, and you can feel how much calmer the switch version is.
Type patterns: matching what an object is
Sometimes you have an object and you want to act based on its actual type. A type pattern does this and even hands you a typed variable to use.
string Describe(object item) => item switch
{
int n => $"An int with value {n}",
string s => $"A string of length {s.Length}",
bool b => $"A bool that is {b}",
null => "Nothing at all",
_ => "Some other thing"
};When item is a string, the arm string s matches, and inside that arm s is already a real string. No casting needed. This is safer than the old way of casting by hand, because the compiler does the work for you. Notice we can even match null as its own arm.
Property patterns: looking inside an object
A property pattern peeks inside an object and checks its fields or properties. Imagine a simple Person record.
record Person(string Name, int Age);
string Ticket(Person p) => p switch
{
{ Age: < 5 } => "Free",
{ Age: >= 5 and < 18 } => "Child fare",
{ Age: >= 60 } => "Senior discount",
_ => "Adult fare"
};The pattern { Age: < 5 } means "a Person whose Age is less than 5." You can read inside the object without writing p.Age again and again. You can even nest deeper, like { Address: { City: "Delhi" } }, to look two levels in.
Picking a ticket price by age
Steps
Person in
A Person object arrives
Read Age
Pattern looks at the Age field
Match band
Find the first age band that fits
Fare out
Return the matching fare
List patterns: matching the shape of a sequence
List patterns arrived in C# 11. They let you match the shape of an array or list. This is handy for parsing.
string Classify(int[] numbers) => numbers switch
{
[] => "Empty list",
[var single] => $"One item: {single}",
[var a, var b] => $"Two items: {a} and {b}",
[1, ..] => "Starts with 1",
[.., 9] => "Ends with 9",
_ => "Something else"
};Here [] matches an empty array. [var single] matches an array with exactly one item and names it single. The .. is a slice pattern that means "any number of items here." So [1, ..] means "starts with 1, then anything."
Case guards with when
Sometimes a pattern is not enough and you need an extra check. The when keyword adds a condition, called a case guard.
string Shipping(Order order) => order switch
{
{ Total: > 1000 } when order.IsMember => "Free express",
{ Total: > 1000 } => "Free standard",
_ => "Paid shipping"
};The first arm only matches when the total is over 1000 and the customer is a member. The guard runs after the pattern matches.
Safety: handle every case
A big reason switch expressions feel elegant is safety. The compiler studies your arms and warns you if they do not cover every possible input. This catches bugs before users ever see them.
If you skip the discard _ and no arm matches at runtime, the switch expression throws a SwitchExpressionException. So adding a final _ => ... arm is a friendly habit. It says "for everything else, do this."
This idea has a fancy name: making invalid states unrepresentable. In plain words, you shape your code so wrong situations simply cannot slip through quietly. Type patterns mixed with property and relational patterns help you remove manual casting and scattered null checks, which makes your code less likely to break.
When to reach for a switch expression
Switch expressions are lovely, but they are not the answer for everything. Here is a simple guide.
| Situation | Good fit? | Why |
|---|---|---|
| Turn one input into one value | Yes | This is exactly what they do |
| Map an enum to text or settings | Yes | Clear, short, and exhaustive |
| Grade marks or band a number | Yes | Relational patterns shine here |
| Run many side effects per case | No | A switch statement is clearer |
| Loop, log, then continue work | No | Statements handle flow better |
A simple rule: if each branch produces a value, use a switch expression. If each branch does a sequence of actions, a switch statement may read better.
A quick word on the C# version
Switch expressions came in C# 8.0. Relational, logical, and property patterns grew through C# 9 and C# 10. List and slice patterns landed in C# 11. After that, C# 12, C# 13, and C# 14 added no new pattern features, so what you learn here is stable and current. With .NET 10 as the long-term support release and C# 14 shipped, switch expressions are a safe everyday tool.
Looking ahead, C# 15 in the .NET 11 preview is adding union types, which pair beautifully with switch expressions for matching one of several known shapes. But you do not need that to start writing elegant code today.
Putting it together: a tiny calculator
Let us finish with one example that uses several patterns at once. A small calculator that takes two numbers and an operator.
double Calculate(double a, double b, string op) => op switch
{
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" when b != 0 => a / b,
"/" => throw new DivideByZeroException(),
_ => throw new ArgumentException($"Unknown op: {op}")
};Read it slowly. Each operator maps to one result. The / case uses a guard so we never divide by zero. The discard arm protects us from any operator we did not plan for. In a handful of lines, the whole calculator is clear, safe, and easy to extend.
Quick recap
- A switch expression takes one input and returns one value, like a station guard pointing you to the right counter.
- It is shorter than a switch statement: the value comes first, arms use
=>, and_is the catch-all. - Pattern matching powers it. You can match by constant, type, relational, property, and list patterns.
- Join patterns with and, or, and not to write clear rules without nested if blocks.
- Use
whento add an extra case guard on top of a pattern. - Always cover every case. The compiler warns you, and a missing case throws a
SwitchExpressionExceptionat runtime. - Use switch expressions when each branch produces a value. Use a switch statement when each branch does many actions.
- The features are stable through C# 14 and .NET 10, so you can rely on them every day.
References and further reading
- switch expression — C# reference (Microsoft Learn)
- Patterns — Pattern matching using is and switch expressions (Microsoft Learn)
- Pattern matching overview (Microsoft Learn)
- Pattern matching tutorial — A tour of C# (Microsoft Learn)
- Resolve pattern matching errors and warnings (Microsoft Learn)
Related Posts
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.
Getting Started with C# Records: A Beginner's Friendly Guide
Learn C# records the easy way: value equality, with expressions, positional syntax, and record struct, explained with simple real-life examples.
C# 15 Union Types: The Easy Guide With Real Examples
Union types in C# 15 (.NET 11) let one method safely return one of many shapes. Learn how they work with simple, real-life examples, diagrams, and a clear comparison with OneOf.
Functional Programming in C#: The Practical Parts You Will Actually Use
A warm, beginner-friendly guide to functional programming in C#: records, immutability, pattern matching, switch expressions, pure functions, and LINQ.
How to Write Better and Cleaner Code in .NET
A beginner-friendly guide to writing better, cleaner C# and .NET code using clear names, small methods, modern C# 14 features, and simple structure.
C# yield return Statement: A Simple Guide With Real Examples
Learn the C# yield return statement the easy way. Understand iterators, lazy evaluation, and deferred execution with simple examples, diagrams, and tables.