C# Switch Case: Syntax, Patterns, and Pitfalls
c# switch case: Learn C# switch case syntax, switch expressions, pattern matching, common mistakes, and performance considerations for clean control flow.
c# switch case requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The switch statement is a core control flow construct in C#. It evaluates a single expression and executes the first case block that matches. Compared to a chain of if statements, switch makes the branching structure explicit and can be easier to read when the number of branches grows. This article covers the traditional switch statement, switch expressions, pattern matching, and the mistakes that commonly appear in real code.
Traditional switch statement syntax
A basic switch statement looks like this:
int value = 2; switch (value) { case 1: Console.WriteLine("One"); break; case 2: Console.WriteLine("Two"); break; default: Console.WriteLine("Other"); break; }
Each case label must end with a break, return, throw, or another jump statement. C# does not allow implicit fall-through from one case to the next. This is a deliberate design choice that prevents the accidental fall-through bugs common in C and C++. If you need to share logic between cases, you can stack case labels:
switch (value) { case 1: case 2: Console.WriteLine("Small"); break; default: Console.WriteLine("Large"); break; }
The default case is optional. It runs when no other case matches. Placing default at the end is conventional, but C# does not require it. The compiler treats default like any other label; its position does not affect matching order.
Switch expressions: a concise alternative
C# 8 introduced switch expressions, which let you return a value from a switch without a separate statement. The syntax is more compact:
string label = value switch { 1 => "One", 2 => "Two", _ => "Other" };
The _ is the discard pattern, equivalent to default. There is no break or return; each arm is an expression. Switch expressions are useful when you need to map an input to an output and want to avoid a separate assignment statement. They also work well in functional-style code where you want to keep the expression pure.
One important difference is that switch expressions require exhaustive handling. If the compiler cannot prove that all possible inputs are covered, it will emit a warning. The discard _ provides a catch-all, but you should use it deliberately.
Pattern matching in switch cases
Modern C# supports pattern matching inside case labels. This goes beyond simple equality checks. You can match on type, property values, relational conditions, and more.
A type pattern:
object obj = "text"; switch (obj) { case string s: Console.WriteLine($"String of length {s.Length}"); break; case int i: Console.WriteLine($"Integer {i}"); break; default: Console.WriteLine("Unknown type"); break; }
A property pattern:
switch (shape) { case Circle { Radius: > 10 } c: Console.WriteLine("Large circle"); break; case Rectangle { Width: var w, Height: var h }: Console.WriteLine($"Rectangle {w}x{h}"); break; default: break; }
A when clause adds a guard condition:
switch (temperature) { case int t when t < 0: Console.WriteLine("Freezing"); break; case int t when t < 20: Console.WriteLine("Cool"); break; default: Console.WriteLine("Warm"); break; }
Pattern matching makes switch much more expressive. You can combine type checks and value checks in a single branch, which often eliminates nested if statements.
Common mistakes and how to avoid them
The most common mistake with switch is forgetting the break statement. In C#, this is a compile-time error, not a runtime bug, which is a good thing. The compiler will refuse to compile code with implicit fall-through unless the case is empty.
Another mistake is relying on the order of case labels when patterns overlap. For example, a case int i will match any integer, so a later case int i when i > 10 will never be reached if the first case appears first. Always order more specific patterns before more general ones.
The default case is sometimes placed at the top, which can confuse readers. While the compiler does not care, conventional placement at the end improves readability. Also, in a switch expression, the discard _ should be placed last to avoid masking other arms.
Finally, remember that switch on a string is case-sensitive by default. If you need case-insensitive matching, you must normalize the input before the switch or use an if chain with StringComparison.OrdinalIgnoreCase.
Performance and maintainability
The C# compiler may compile a switch statement on an integer or enum into a jump table, giving O(1) lookup time. For string switches, the compiler can generate a hash-based dispatch. However, these optimizations are not guaranteed and depend on the number of cases and the compiler's heuristics. In most applications, the difference between a switch and an equivalent if chain is negligible. The real benefit of switch is maintainability: it forces a single expression to be evaluated once and makes the branching structure visually explicit.
When the number of cases grows large, a switch becomes harder to read. If you find yourself adding dozens of cases, consider using a dictionary of delegates or a strategy pattern. That said, switch remains a good default for a moderate number of branches.
When to use switch vs if-else chains
Use switch when you are comparing one value against a fixed set of constants or patterns. This includes enums, integers, strings, and type checks. Use if when the condition involves multiple variables, ranges that are not easily expressed as patterns, or complex boolean logic.
For example, a range check like x > 0 && x < 10 is clearer with if. A type dispatch is often clearer with switch. The decision also depends on whether you need to return a value; switch expressions make the intent obvious.
Advanced patterns and edge cases
C# 9 added relational patterns and logical patterns. You can write case < 0 or case >= 0 and <= 100. These combine naturally with when clauses.
A recursive pattern can match nested structures:
switch (point) { case (int x, int y) when x == y: Console.WriteLine("On diagonal"); break; default: break; }
The var pattern captures the value without checking its type:
case var v: Console.WriteLine(v);\n break; }
This is rarely useful alone but can be combined with a when clause.
One edge case: a switch expression must be exhaustive. If you omit the discard _ and the compiler cannot prove exhaustiveness, you get a warning. In some cases, you can use a throw expression as an arm to handle unexpected input:\n```csharp
string result = value switch
{
1 => "One",
2 => "Two",
_ => throw new ArgumentOutOfRangeException(nameof(value))
};
This is a clean way to signal invalid input.