Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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 if chain. 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 Book or a Game, an order over $100 with no discount code, a list that starts with "add") → patterns inside is or 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 virtual method with an override per type. A type switch over your own classes is often a missing virtual method.

Two-way choices

One decision, a chain of them, and when nesting beats combining conditions.

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

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

Each derived type brings its own version, and the call picks it at run time with no switch at all.