Back to Blog
C#

C# Switch Statement: Syntax, Patterns, and Pitfalls

c# switch statement: Learn the C# switch statement syntax, switch expressions, pattern matching, common pitfalls, and when to use switch over if-else or dictionaries.

switch statementpattern matchingswitch expressionscontrol flowcode readability
Illustration of a C# switch statement symbol surrounded by multiple branching paths, representing pattern matching and control flow decisions.

The C# switch statement is a control flow construct that selects one of several code blocks based on the value of an expression. Unlike a series of if-else checks, switch makes the intent explicit and can be compiled more efficiently in some cases. This article covers the classic switch statement, the more concise switch expression introduced in C# 8, pattern matching capabilities, common mistakes, and the tradeoffs you should consider when choosing between switch and other branching approaches.

The Basic C# Switch Statement

The classic switch statement evaluates a single expression and compares it against a set of case labels. When a match is found, the corresponding block executes. Here is the minimal syntax:

int code = GetStatusCode(); switch (code) { case 200: Console.WriteLine("OK"); break; case 404: Console.WriteLine("Not Found"); break; default: Console.WriteLine("Other"); break; }

Every non-empty case block must end with a break, return, throw, or goto to prevent fall-through. This is a deliberate design choice: C# does not allow implicit fall-through like C or C++. If you need multiple labels for the same block, you can stack them:

switch (code) { case 200: case 201: Console.WriteLine("Success"); break; default: Console.WriteLine("Other"); break; }

The default case is optional but recommended. It catches values that do not match any explicit case, which is especially important when the input comes from an external source.

Switch Expressions: A More Concise Form

C# 8 introduced switch expressions, which turn the statement into an expression that produces a value. This is useful when you want to assign a result directly without a series of assignments inside each case block.

string status = code switch { 200 => "OK", 404 => "Not Found", _ => "Other" };

The _ character is the discard pattern, equivalent to default. Switch expressions must be exhaustive; if no pattern matches and there is no discard, the compiler will not produce an error but the runtime will throw a SwitchExpressionException. Always include a discard arm unless you can prove that the input is constrained.

Switch expressions work well with pattern matching, allowing you to check types and conditions inline.

Pattern Matching in Switch

Both switch statements and switch expressions support pattern matching, which goes beyond simple equality. You can match on types, relational conditions, and logical combinations.

Type Patterns

object value = GetValue(); string description = value switch { int i => $"Integer: {i}", string s => $"String: {s}", _ => "Unknown" };

Relational and Logical Patterns

C# 9 added relational patterns (<, >, <=, >=) and logical patterns (and, or, not). These are especially useful for range checks:

int score = 85; string grade = score switch { >= 90 => "A", >= 80 => "B", >= 70 => "C", >= 60 => "D", _ => "F" };

You can combine patterns with and and or:

int temperature = 25; string condition = temperature switch { < 0 or > 40 => "Extreme", >= 10 and <= 30 => "Mild", _ => "Moderate" };

Pattern matching makes the switch statement far more expressive, but it also introduces a subtlety: the order of arms matters because patterns are evaluated top-down. The first matching arm wins. Place more specific patterns before broader ones.

Common Pitfalls and How to Avoid Them

Fall-Through and Missing Break

Forgetting a break or return in a switch statement is a common error. The compiler will reject the code if you attempt implicit fall-through, but you might still accidentally leave a case empty when you meant to share logic. Stacking labels is the correct way to share a block.

Variable Scope

Variables declared inside a case block are scoped to that block. If you need to use the same variable name across multiple cases, declare it outside the switch. This is a frequent source of compilation errors.

switch (code) { case 200: int retries = 0; // local to this case break; case 500: // retries is not accessible here break; }

Exhaustiveness in Switch Expressions

Switch expressions must cover all possible inputs. If you omit the discard arm and the input falls through, the runtime throws SwitchExpressionException. This is a runtime failure, not a compile-time one, so it can slip into production. Always include a discard arm unless you are absolutely certain the input is constrained by an enum or a sealed hierarchy.

Using Switch on Nullable Types

When the input is a nullable value type, a case null is distinct from a case 0. The compiler treats null as a separate pattern. If you want to treat null and zero together, you need an explicit or pattern.

Performance and Runtime Behavior

The C# compiler optimizes switch statements based on the type of the governing expression. For integer types, it often generates a jump table, which is O(1) lookup. For strings, it may use a hash-based approach or a series of comparisons depending on the number of cases. These details are implementation-specific and can change between .NET versions.

The practical takeaway is that a switch with many integer cases is usually faster than a chain of if-else comparisons. For strings, the difference is less pronounced, and readability should guide your choice. Pattern matching with complex patterns may prevent the compiler from using a jump table, but the performance impact is rarely significant unless the switch is in a hot loop with thousands of iterations.

Do not micro-optimize switch statements without profiling. The clarity of a switch often outweighs any microsecond gains. If you are measuring a bottleneck, use a profiler to confirm that the switch is the cause before rewriting it.

When to Use Switch Over If-Else or Dictionaries

Switch is the right tool when you are branching on a single value and the number of cases is moderate. It reads more cleanly than a long if-else chain because the structure is explicit.

If-else is better when conditions are complex and involve multiple variables or ranges that are not easily expressed as patterns. For example, if (a > 0 && b < 10) is clearer than a switch with a tuple pattern.

A dictionary is a good alternative when you are mapping input values to output values without side effects. For instance, a lookup table of status codes to messages:

Dictionary<int, string> messages = new() { [200] = "OK", [404] = "Not Found" };

Dictionaries are particularly useful when the mapping is dynamic or loaded from configuration. However, they do not support pattern matching or complex conditions. Use a switch when you need to execute different logic per case, not just return a value.

A final consideration is maintainability. A switch with dozens of cases becomes hard to read. If you find yourself adding many cases frequently, consider polymorphism or a strategy pattern instead. The switch statement is not inherently bad, but it should not become a dumping ground for every new condition.

Advanced Pattern: Recursive Patterns in Switch

C# supports recursive patterns, which allow you to match nested objects. This is particularly useful for working with discriminated unions or when you need to deconstruct a type within a case.

public record Point(int X, int Y); string Describe(object shape) => shape switch { Point { X: 0, Y: 0 } => "Origin", Point { X: var x, Y: var y } => $"Point at ({x}, {y})", _ => "Unknown" };

This combines type matching with property patterns. The compiler checks the type and then evaluates the property conditions. Recursive patterns keep related logic together and can eliminate nested if statements.

When using recursive patterns, be mindful of the order. A more specific pattern like Point { X: 0, Y: 0 } must come before the general Point pattern. The compiler will warn if a later pattern can never match because an earlier one already covers it.

Recursive patterns are a powerful feature, but they can reduce readability if overused. Use them when the shape of the data is stable and the pattern expresses the business rule clearly. For complex object graphs, a dedicated method or a visitor might be more maintainable.

c# switch statement: Practical Usage and Code Examples | RYUSLOG DEV