Back to Blog
C#

C# Switch Expression: Syntax and Practical Usage

Learn how the C# switch expression provides concise, pattern-based branching. This article covers its syntax, patterns, when to prefer it over a switch statement, common pitfalls, and compatibility notes.

C#switch expressionpattern matchingC# 8control flow
A clean illustration showing a single input branching into multiple labeled output paths, representing the C# switch expression's pattern-based mapping.

The C# switch expression is a concise alternative to the traditional switch statement. It evaluates a single input and produces a value, which makes it natural for mapping, validation, and state transitions. Unlike the statement form, the expression form does not require break or return statements, and it can be assigned directly to a variable or returned from a method.

Basic Syntax and First Example

The switch expression uses a => arrow to connect each pattern to its result. The input expression appears before the switch keyword, and the list of arms appears inside braces. A discard pattern _ acts as the default case.

string GetDayName(int day) { return day switch { 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 => "Thursday", 5 => "Friday", 6 => "Saturday", 7 => "Sunday", _ => "Invalid day" }; }

Each arm's right side is an expression that produces the overall result. The compiler checks that all arms are compatible with the target type. If an arm throws an exception, the throw expression is allowed because it can be converted to any type, but the remaining arms still need a consistent result type. Usually include a discard arm (_) to handle inputs that do not match any arm. Unless the compiler can prove that every possible input is covered, it may warn that the expression is not exhaustive, and the runtime throws if no arm matches.

Pattern Matching in Switch Expressions

Switch expressions support several pattern forms beyond constant patterns. Type patterns let you branch based on the runtime type of the input, and property patterns can inspect fields or properties. The following example uses a property pattern with a nested relational pattern:

decimal CalculateDiscount(Order order) { return order switch { { Total: > 1000 } => 0.10m, { Total: > 500 } => 0.05m, _ => 0m }; }

The property pattern { Total: > 1000 } matches when the Total property is greater than 1000. This relational pattern is supported in C# 9 and later. For C# 8, which lacks relational patterns, you can write the same conditions with a type pattern and a when clause:

decimal CalculateDiscount(Order order) { return order switch { Order o when o.Total > 1000 => 0.10m, Order o when o.Total > 500 => 0.05m, _ => 0m }; }

The when clause adds a boolean condition to any pattern. It is evaluated only after the pattern itself matches. This keeps the logic readable and avoids nested if statements.

When to Use a Switch Expression vs a Switch Statement

The switch expression is best when you need to produce a single value from a set of alternatives. It is idiomatic for mapping codes to labels, parsing enums, or selecting configuration. The switch statement remains better when each arm needs to execute multiple statements, or when you need goto-style fall-through, which is not supported in the expression form.

ScenarioSwitch ExpressionSwitch Statement
Produce a single valueYesPossible but verbose
Execute multiple statements per armNoYes
Fall-through behaviorNot supportedSupported via goto
Pattern matchingRichSupported since C# 7
Readability for simple mappingHighLower

If you need to perform side effects like logging, database writes, or multiple assignments, the statement form is usually clearer. The expression form forces every arm to yield a value, which can lead to awkward code if you are only interested in the side effect.

Common Pitfalls and Edge Cases

One common mistake is forgetting the discard arm. If the input is an enum and you handle all named members, the compiler may still warn about a missing default because enum variables can hold numeric values that are not defined by any member. [Flags] enums are especially likely to produce such warnings. Null does not match a type pattern unless you handle it with a null pattern or a discard arm. For example, the following code returns "unknown" for null input:

string Describe(object value) { return value switch { int i => "integer", string s => "string", _ => "unknown" }; }

If you want to treat null specially, add a null pattern before the discard arm. The order of arms matters because patterns are evaluated top to bottom. The first matching arm wins, so put more specific patterns before general ones.

Another edge case is the when clause with a type pattern. The variable declared in the pattern is in scope only inside the when clause and the arm body. Attempting to use it outside the arm causes a compile error.

Performance and Allocation Considerations

The switch expression compiles to a jump table or a series of comparisons, depending on the patterns. For constant patterns, the compiler may generate a dictionary lookup or a binary search, similar to a traditional switch. For complex patterns like type or property patterns, the runtime performs type checks and property reads. There is no inherent allocation cost in the switch expression itself; the input is not boxed unless you are switching on a value type stored in an object variable.

If you are switching on a string, the compiler may use a hash-based lookup, which is efficient for large sets of strings. For enums, the generated code is a simple integer comparison. Avoid using switch expressions in hot loops with property patterns that read the same property repeatedly; the compiler does not cache the property value across arms. If the property is expensive to compute, assign it to a local variable before the switch expression.

Compatibility and Version Requirements

The switch expression was introduced in C# 8.0 and is supported by the compilers in .NET Core 3.0 and later, as well as .NET 5 and later. Most switch expressions compile to the same jumps and comparisons you could write manually, so the basic feature does not depend on a new runtime method. One runtime behavior to know: if no arm matches and there is no discard arm, the generated code can throw SwitchExpressionException; that exception type may not exist on all older runtimes, so test on your target runtime.

Relational patterns (>, <) and not patterns were added in C# 9.0. Property patterns with nested relational patterns also require C# 9.0. Check your project's language version to ensure the patterns you use are supported.

When targeting older frameworks, you can use switch expressions with constant patterns if the compiler supports C# 8.0. Relational, not, and logical patterns require C# 9.0 or later; if the project language version is too low, the compiler reports an error, not just a warning.

Real-World Usage Patterns

A common use is mapping a status enum to a user-facing message. The switch expression keeps the mapping in one place and prevents scattered if statements.

string GetStatusMessage(OrderStatus status) { return status switch { OrderStatus.Pending => "Your order is waiting for payment.", OrderStatus.Shipped => "Your order has been shipped.", OrderStatus.Delivered => "Your order was delivered.", _ => "Unknown status." }; }

Another use is state machine transitions. The expression can return the next state based on the current state and an event. This pattern is compact but can become hard to read if the state machine has many transitions. In that case, a dictionary of transitions or a dedicated class may be more maintainable.

Switch expressions also work well with tuples. You can switch on multiple inputs by using a tuple pattern:

string GetMove((int x, int y) direction) { return direction switch { (0, 1) => "up", (0, -1) => "down", (1, 0) => "right", (-1, 0) => "left", _ => "invalid" }; }

The tuple pattern is evaluated positionally, and the discard pattern covers any other combination. This is cleaner than nested switch statements or if chains.

Advanced: Combining with Other Features

The switch expression can be combined with when clauses, type patterns, and recursive patterns to handle complex input. For example, if you define a Maybe type or a discriminated union, these patterns allow concise case handling. The following example uses type patterns to process a shape:

double Area(Shape shape) { return shape switch { Circle c => Math.PI * c.Radius * c.Radius, Rectangle r => r.Width * r.Height, Triangle t => 0.5 * t.Base * t.Height, _ => throw new ArgumentException("Unknown shape", nameof(shape)) }; }

Each arm can call a method or perform a calculation. The throw expression in the discard arm is useful when the input should never reach that case. This makes the switch expression a concise way to implement exhaustive handling without relying on a base class virtual method.

One limitation is that the switch expression does not support goto or fall-through. Each arm must be a self-contained expression. If you need to share logic between arms, extract that logic into a local function or a method and call it from each arm. This keeps the expression readable and avoids duplicating code.

C# Switch Expression: Syntax, Patterns, and Practical Usage | RYUSLOG DEV