switch Statements and Expressions
By the end of this lesson
Replace long condition chains with a switch, and use switch expressions for value selection.
When several branches all test the same value, a switch says so directly. An else if chain leaves the reader checking each condition to confirm they are about one thing.
C# has two forms. A switch statement runs different code per case. A switch expression produces a single value. Which one you want depends on that distinction.
string status = "Dispatched";
switch (status)
{
case "Draft":
Console.WriteLine("Not submitted yet.");
break;
case "Submitted":
case "Approved":
Console.WriteLine("Waiting for the warehouse.");
break;
case "Dispatched":
Console.WriteLine("On its way.");
break;
default:
Console.WriteLine($"Unrecognised status: {status}");
break;
}- Each case ends with break, which leaves the switch. C# does not allow one case to run on into the next — omit the break and the build fails with "control cannot fall through".
- Two labels stacked together share one block. That is how you say "either of these".
- default catches everything else. Include it: statuses get added over time, and doing nothing quietly is a poor response to one you do not recognise.
switch expressions produce a value
decimal orderTotal = 640m;
decimal discountRate = orderTotal switch
{
>= 1000m => 0.10m,
>= 500m => 0.05m,
>= 100m => 0.02m,
_ => 0m,
};
Console.WriteLine($"Discount rate: {discountRate:P0}");- The value being tested comes first, then switch, then a list of arms in braces.
- Each arm is a pattern, then =>, then the value it produces. There is no break, because nothing is being executed — the whole construct evaluates to one value.
- >= 1000m is a relational pattern, which is what lets bands be written without a chain of ifs.
- Arms are tried top to bottom and the first match wins, so the largest band has to come first. Put >= 100m at the top and every order gets 2 per cent.
- _ is the catch-all. Leave it out and a value matching no arm throws SwitchExpressionException while the program is running.
The two forms, side by side:
| switch statement | switch expression | |
|---|---|---|
| What it is for | Running different code per case | Producing one value |
| Shape of a branch | case X: ... break; | pattern => value, |
| Needs break | Yes, on every case | No |
| Value that matches nothing | Falls to default, or the switch does nothing | Throws unless there is a _ arm |
| Several statements in a branch | Yes | No — it has to be a single expression |
| Fits | Side effects and multi-step work | Assignment and return statements |
object payment = new CardPayment(120m, "VISA");
string summary = payment switch
{
CardPayment card when card.Amount > 100m => $"Large card payment on {card.Scheme}",
CardPayment card => $"Card payment on {card.Scheme}",
BankTransfer transfer => $"Transfer from sort code {transfer.SortCode}",
null => "No payment recorded",
_ => "Unrecognised payment type",
};
Console.WriteLine(summary);
public class CardPayment
{
public CardPayment(decimal amount, string scheme)
{
Amount = amount;
Scheme = scheme;
}
public decimal Amount { get; }
public string Scheme { get; }
}
public class BankTransfer
{
public BankTransfer(string sortCode)
{
SortCode = sortCode;
}
public string SortCode { get; }
}- A type pattern tests the type and gives you a typed variable in one step, so card.Scheme is available inside that arm.
- when adds a further condition to an arm. The narrower arm has to come first — swap the two CardPayment arms and the plain one answers for every card, large or not.
- null is a pattern in its own right. It is the branch people most often leave out, and the one that produces a NullReferenceException later.
- Read top to bottom and you are reading the rules in priority order. That readability is most of the reason to prefer this over an if chain.
Summary
- A switch statement runs code per case; a switch expression produces a single value
- Arms and cases are tested in order, so the most specific condition goes first
- C# does not allow fall-through — break, return or throw ends every case
- A switch expression without a _ arm throws at run time on an unmatched value
- Type patterns and when clauses let one switch replace a nested chain of type checks
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Rewrite this as a switch expression: if the delivery region is "London" the lead time is 1 day, "South East" is 2, "Scotland" is 4, and anything else is 3.
Then add a branch for a null region that produces -1, and decide what a caller should do with that.
Show solution
A switch expression keeps each region and its lead time on one line, which makes the whole table readable at a glance and easy to extend.
The null arm has to be listed explicitly; the _ arm would otherwise absorb it and report 3 days for a region nobody supplied.
-1 as a sentinel is honest but awkward, because every caller has to know it means "unknown". A nullable int return, or throwing, both express it better. That choice comes up again in the Return Values lesson.
string? region = "Scotland";
int leadTimeDays = region switch
{
"London" => 1,
"South East" => 2,
"Scotland" => 4,
null => -1,
_ => 3,
};
Console.WriteLine(leadTimeDays == -1
? "No region supplied."
: $"Lead time: {leadTimeDays} day(s)");Challenge
Challenge
A shipping charge depends on two things at once: the parcel weight band and whether the customer is a member.
Write it as a single switch expression that tests both values together, rather than nesting a switch inside an if.
Show solution
Putting both values in brackets makes a tuple, and each arm then matches a pair. The result is a rate card you can read as a grid, with the member and non-member cases sitting next to each other.
Relational patterns work inside a tuple pattern, so the weight bands do not need separate handling.
The trade-off is that a tuple with three or four elements stops being readable. At that point a small class with named properties, or a lookup table, carries the meaning better than positional matching.
decimal weightKg = 12m;
bool isMember = true;
decimal shipping = (weightKg, isMember) switch
{
( <= 2m, true) => 0m,
( <= 2m, false) => 2.99m,
( <= 20m, true) => 4.99m,
( <= 20m, false) => 7.99m,
(_, true) => 12.99m,
(_, false) => 16.99m,
};
Console.WriteLine($"Shipping: {shipping:N2}");Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.