Back to Blog
C#

C# Switch Expression: Syntax and Practical Usage

c# switch expression: Learn how to use the C# switch expression for concise, pattern-based branching, including syntax, patterns, tradeoffs, and common pitfalls.

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 is an expression that must match the input type. The compiler enforces that the result type is the same across all arms. If an arm throws an exception, the throw expression is allowed, but the overall type must still be consistent. The discard arm is required unless the compiler can prove all possible inputs are covered, which is rarely possible for arbitrary integers.

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 type pattern and a when clause to add a condition:

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 is a relational pattern, which is supported in C# 9 and later. For earlier C# versions, you would need a when clause:

decimal CalculateDiscount(Order order) { return order switch { Order o when 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 members, the compiler may still warn about a missing default when the enum has a [Flags] attribute or when the input can be null. For nullable types, the null value does not match any pattern unless you add a null pattern or the discard arm. The following code throws ArgumentNullException for null input:

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

If value is null, the type patterns do not match, and the discard arm returns "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. It is available in .NET Core 3.0 and later, as well as .NET 5 and beyond. If you are using an older framework, you can still use the syntax as long as the compiler supports C# 8.0, but the runtime does not need special support because the feature is syntactic sugar. However, some pattern forms like 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 the switch expression with constant patterns, but avoid relational and logical patterns unless you set <LangVersion> to a newer value. The compiler may emit a warning if the language version is lower than the feature requires.

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, you can match on a Maybe type or a a discriminated union. The following example uses a type pattern and a property pattern 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: Practical Usage and Code Examples | RYUSLOG DEV