Back to Blog
C#

c# switch expression return value

c# switch expression return value: Learn how to use C# switch expressions to produce return values concisely, with pattern matching, edge cases, and production tradeoffs.

C# switch expressionsReturn valuesC# patternsSwitch expressionC# control flow
Illustration of a C# switch expression mapping an input to a return value, with arrows indicating pattern matching branches.

When a developer searches for c# switch expression return value, the intent is usually to understand how a switch expression can be used to assign a value directly to a variable or return it from a method. Unlike the traditional switch statement, a switch expression is an expression that evaluates to a value. This article explains the syntax, practical usage patterns, common pitfalls, and the maintainability and runtime considerations that matter when choosing between a switch expression and a classic switch statement.

Syntax of a Switch Expression

A switch expression uses the switch keyword and arrow (=>) separators between each arm and its result. The expression evaluates to one of the arm values. Here is the minimal syntax:

int GetPriority(string level) { return level switch { "low" => 1, "medium" => 2, "high" => 3, _ => 0 }; }

The input expression (here level) appears before the switch keyword. Each arm consists of a pattern, followed by =>, followed by the value or expression to return. The underscore (_) is the discard pattern, which acts as the default case. The entire switch expression is terminated by a semicolon after the closing brace.

Unlike a switch statement, there is no break, return, or default: label. Every arm must produce a value, and the expression must be assigned or returned. This makes the intent clearer and eliminates the risk of falling through cases, a common source of bugs in traditional switch statements.

Returning Values from Methods

Switch expressions are particularly useful when you need to map an input to a result in a single expression. Consider a method that returns a text representation based on an enum value:

public string GetStatusMessage(OrderStatus status) { return status switch { OrderStatus.Pending => "Order is pending.", OrderStatus.Shipped => "Order has shipped.", OrderStatus.Delivered => "Order delivered.", OrderStatus.Cancelled => "Order cancelled.", _ => "Unknown status." }; }

Because the switch expression is an expression, it can also be used directly in an assignment, a function argument, or a lambda body. This reduces boilerplate and keeps the logic declarative. The compiler also analyzes the possible patterns and emits a warning if not all possible inputs are covered by a pattern, prompting developers to add a discard pattern.

Pattern Matching in Switch Expressions

Switch expressions support the full range of C# pattern matching, not just constant patterns. This includes relational patterns, type patterns, and property patterns. These patterns make the expression more expressive and reduce the need for nested if-else chains.

Type Pattern Example

Suppose you have a method that processes a shape and returns its area. Using a switch expression with type patterns:

public double GetArea(Shape shape) { return shape switch { Circle c => Math.PI * c.Radius * c.Radius, Rectangle r => r.Width * r.Height, Triangle t => (t.BaseLength * t.Height) / 2, _ => throw new ArgumentException($"Unknown shape: {shape.GetType().Name}") }; }

In this example, each arm matches a specific type and then uses the variable (e.g., c, r, t) to access properties. The discard pattern throws an exception instead of returning a value, illustrating that the expression can contain any expression on the right side of =>, including throw.

Relational Pattern Example

Relational patterns allow comparisons using <, >, <=, >=. For example, classify a temperature score:

public string ClassifyTemperature(int celsius) { return celsius switch { <= -10 => "Very cold", < 0 => "Cold", < 15 => "Mild", < 25 => "Warm", _ => "Hot" }; }

The patterns are evaluated in order, and the first matching arm produces the value. The discard pattern catches any remaining input, ensuring a return value exists.

When the Input Is a Collection or Multiple Values

Switch expressions can also evaluate tuples, which is useful when a decision depends on multiple inputs. For example, determine a color based on two boolean flags:

public string GetColor(bool isPrimary, bool isDark) { return (isPrimary, isDark) switch { (true, true) => "Dark primary", (true, false) => "Light primary", (false, true) => "Dark secondary", (false, false) => "Light secondary" }; }

The left side of the switch is a tuple literal. Each arm uses a positional pattern that matches the tuple's elements. This approach avoids nested conditionals and is more readable than a series of if statements.

Common Pitfalls: Return Type Consistency

One of the most frequent mistakes is having arms that return values of different types. The compiler infers a common type for the entire expression, and if the types do not have a common type, compilation fails. For example:

object GetValue(bool flag) { return flag switch { true => "text", // string false => 42 // int }; }

This code compiles because the target type is object, and there is an implicit conversion from both string and int to object. However, if the method's return type is int, the arm with the string would cause a compilation error. The rule is that every arm must be implicitly convertible to a common type, typically the target type of the expression.

Another pitfall is forgetting that the default (discard) pattern is optional unless the compiler can prove that the set of patterns is exhaustive. For example, with a non-nullable reference type, the compiler still warns if the input could be a null reference and you haven't handled it. In recent C# versions, nullable analysis helps to ensure that a null value is handled explicitly, either by a null pattern or by a discard pattern that covers all remaining cases.

Performance Considerations

The runtime cost of a switch expression is generally the same as the equivalent switch statement. The compiler can emit efficient jump tables or a series of comparisons, depending on the patterns. However, you should not assume that a switch expression is always faster than an if chain; it depends on the complexity of the patterns and the JIT's optimization. In most production scenarios, the readability and maintenance benefits outweigh any micro-optimization differences.

One relevant performance aspect is the allocation of boxed values or delegates if the arms involve conversions. For example, if the method returns object and each arm creates a new value type, boxing occurs. But this is identical to what happens with a switch statement.

For CPU-bound paths where the switch expression is called millions of times, you might measure the generated IL to confirm no unexpected allocations. In practice, the JIT treats switch expressions similarly to switch statements, so the primary cost is often the pattern evaluation itself, especially with complex property patterns. If you notice hotspots, use benchmark tools to guide the decision, but do not prematurely rewrite code based on speculative performance.

Maintainability and Why It Matters

Switch expressions contribute to maintainability because they keep the mapping between input and output in a single place, making it easier to add or remove cases. With a traditional switch statement, each case often ends with a return, and it's easy to forget a break or accidentally fall through. A switch expression eliminates those failure modes by design.

Consider the evolution of an order status handler. Adding a new status in a switch expression requires adding one arm; the compiler then warns you if the set of patterns is not exhaustive. That early feedback is valuable, especially when the input is an enum with a finite set of values.

However, a switch expression is not always the best choice. If the logic inside each case is complex—involving many statements, variable declarations, or side effects—a switch statement is clearer. The expression form is best suited for mapping inputs to outputs, not for executing blocks of code. For example, if each case involves logging multiple lines and updating several fields, a standard switch statement reads better.

Another maintainability concern is readability when the patterns get nested. Long chains like (x, y) switch { (0, 0) => ... } are fine for a small set of tuples, but if the input has many fields, consider using a property pattern or a dedicated method. Overly complex expressions can hurt readability, and a well-named method with a switch statement may be preferable.

Choosing Between Switch Statement and Switch Expression

The decision between a switch statement and a switch expression hinges on the shape of the logic. Use a switch expression when the code maps an input to a value and each arm produces a single expression. Use a switch statement when each case requires multiple statements, local scoped variables, or a sequence of operations with control flow (like break or goto).

ConcernSwitch expressionSwitch statement
Produces a valueYes, directlyRequires return or assignment
Fall-through behaviorNot possiblePossible if misused
Complex per-case logicAwkwardSuitable
Exhaustiveness checkCompiler warning for missing patternsNo automatic check
Best forMapping inputs to outputsMulti-step processing per case

A switch expression is more concise, but it is not inherently more performant. The choice should be driven by clarity and maintainability, not by assumptions about efficiency.

Advanced Usage: Property and Var Patterns

Beyond constant and type patterns, switch expressions support property patterns that allow you to match on an object's properties directly. For example, classify a point:

public string ClassifyPoint(Point point) { return point switch { { X: 0, Y: 0 } => "Origin", { X: 0 } => "On Y-axis", { Y: 0 } => "On X-axis", _ => "In quadrant" }; }

The property pattern checks whether point.X equals 0 and point.Y equals 0, and so on. This can replace nested if checks with a single, readable expression.

Var patterns are also useful when you want to bind the matched value to a new variable without actually testing its type—essentially a fallback that captures the value. For instance:

public string Describe(object obj) { return obj switch { int i when i > 10 => "Big integer", string s => $"String of length {s.Length}", var other => $"Unknown type: {other.GetType().Name}" }; }

The var other pattern matches any value and binds it to other for use in the arm. It is equivalent to the discard pattern in terms of matching, but it allows you to reference the value, which the discard pattern does not.

Compatibility and Language Version Dependencies

Switch expressions were introduced in C# 8.0 and have been refined in later versions. If you are working in an older codebase that targets an earlier language version, you cannot use them without upgrading the language version setting. This is a practical constraint: the compiler's LangVersion must be at least 8.0. .NET Core 3.x and .NET 5+ support this feature natively, but if you are targeting the .NET Framework, you need C# 8.0 support through the Roslyn compiler, and you must use the .NET Core SDK or a compatible compiler. Always verify the target framework's compatibility when adopting switch expressions.

Additionally, the behavior of null handling changed with nullable reference types. When the input is a reference type, ensure you handle null explicitly if the compiler's null-state analysis requires it. Use the null pattern or a discard pattern that covers null. For example:

public string GetOrDefault(string? value) { return value switch { null => "default", _ => value }; }

This code explicitly handles a null input, avoiding a null reference exception if the null pattern were omitted.

Switch expressions are a powerful tool for producing return values concisely, but they are most effective when the logic fits the expression form. As with any language feature, the best choice depends on the specific code structure and the maintainability tradeoffs that matter for your team.