Every way to branch on a value
if and else, the conditional operator, the switch statement, the switch expression, patterns and virtual methods. Same decision, different shapes.
Look at what each branch produces, then at what the conditions test.
- Pick one of two values → the conditional operator:
string label = stock > 0 ? "In stock" : "Sold out"; - Run different statements for conditions that test unrelated
things (the file exists, the user is signed in, the cart is
empty) → an
if/else ifchain. It's also the choice when a branch has side effects or returns early. - Compute one result from one value, with many cases (a status code to a label, a shipping zone to a rate) → a switch expression. You get first-match order, a warning when a case is missing, and an error when an arm can never match.
- Run different blocks for many cases of one value → a switch statement.
- The cases depend on the value's type or shape (a
Bookor aGame, an order over $100 with no discount code, a list that starts with "add") → patterns insideisor a switch: type, relational, property, positional and list patterns. - The cases are the types of your own class hierarchy and each
type owns its behaviour → a
virtualmethod with an override per type. A type switch over your own classes is often a missing virtual method.
Two-way choices
Section
if, else and else if chains
One decision, a chain of them, and when nesting beats combining conditions.
Section
The conditional operator, ? :
An if/else that produces a value, so it fits inside an expression.
Formal. a && b evaluates b only when a is true; a || b
evaluates b only when a is false. The null-coalescing operator
a ?? b evaluates b only when a is null, and a?.M() skips the
member access when a is null. The non-short-circuit & and | on
bool always evaluate both operands.
In plain English. C# stops as soon as it knows the answer. If the first half of an "and" is already false, the whole thing is false, so the second half never runs.
Example. if (name != null && name.Length > 0) is safe: when name
is null, name.Length is never touched.
Why this rule exists. It makes guard conditions safe to write in one line, and it means code on the right-hand side is not guaranteed to run.
&& and || stop as soon as the answer is known, which is what makes s != null && s.Length > 0 safe.
Many-way choices
One runs a block per case, the other produces a value. No fall-through in either.
Formal. A switch expression tests its arms in the order written and
produces the value of the first arm whose pattern (and optional when
guard) matches. An arm that can never be reached because earlier arms
already cover it is a compile error. If the arms don't cover every
possible input the compiler warns (CS8509), and an unmatched value
throws SwitchExpressionException at run time. A discard arm _ =>
covers everything that is left.
In plain English. C# checks the choices top to bottom and takes the first one that fits. Put the specific cases first and the catch-all last, and the compiler will tell you if you forgot a case.
Example. score switch { >= 90 => "A", >= 80 => "B", _ => "C" }
gives "A" for 95, because >= 90 is tried before >= 80.
Why this rule exists. Order-dependent matching is what lets arms overlap safely, and the coverage check catches the forgotten case before a user does.
Arms are tried top to bottom and the first match wins, so specific cases go first.
Exhaustiveness, arm order, and the trade-off between adding types and adding operations.
Branching on type and shape
Section
Relational and logical patterns
Ranges like >= 18 and < 65, combined with and, or and not.
Test an object's properties or a tuple's parts, and add a when clause for anything a pattern can't say.
Section
List patterns
Match a sequence by its shape: ["add", var name, ..] for a typed command.
Letting the type decide
Section
Virtual methods and override
Each derived type brings its own version, and the call picks it at run time with no switch at all.