Better way to handle switch case in C#
Categories:
Beyond Basic Switch: Modern C# Techniques for Cleaner Conditional Logic
Explore advanced C# patterns and features to replace traditional switch statements, improving readability, maintainability, and extensibility in your code.
The switch
statement has been a staple in C# for handling multiple conditional branches. While effective for simple scenarios, it can become cumbersome and lead to less maintainable code as complexity grows. Modern C# versions (C# 7.0 and later) introduce powerful features like pattern matching, switch expressions, and property patterns that offer more elegant and expressive ways to handle conditional logic. This article will guide you through these modern alternatives, demonstrating how to write cleaner, more robust code.
The Traditional Switch Statement: Limitations and Challenges
Before diving into modern alternatives, let's briefly review the traditional switch
statement and its common pitfalls. A classic switch
works well with primitive types and enums, but it can struggle with type checking, null handling, and complex conditions, often requiring boilerplate code or if-else if
chains within case
blocks.
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
public string GetStatusDescription(OrderStatus status)
{
switch (status)
{
case OrderStatus.Pending:
return "Order is awaiting confirmation.";
case OrderStatus.Processing:
return "Order is being prepared for shipment.";
case OrderStatus.Shipped:
return "Order has left the warehouse.";
case OrderStatus.Delivered:
return "Order has been successfully delivered.";
case OrderStatus.Cancelled:
return "Order was cancelled by the customer or system.";
default:
return "Unknown order status.";
}
}
A traditional switch
statement handling an enum.
switch
statements are perfectly valid, they can become verbose, especially when dealing with multiple conditions per case or when needing to check types or properties of objects.Embracing Switch Expressions (C# 8.0+)
Switch expressions provide a more concise syntax for switch
statements that return a value. They eliminate the need for break
statements and allow for more compact code, especially when combined with pattern matching. This significantly improves readability for scenarios where you're mapping an input to an output.
public string GetStatusDescriptionModern(OrderStatus status) =>
status switch
{
OrderStatus.Pending => "Order is awaiting confirmation.",
OrderStatus.Processing => "Order is being prepared for shipment.",
OrderStatus.Shipped => "Order has left the warehouse.",
OrderStatus.Delivered => "Order has been successfully delivered.",
OrderStatus.Cancelled => "Order was cancelled by the customer or system.",
_ => "Unknown order status."
};
Using a switch expression for the same enum handling logic.
flowchart TD A[Input: OrderStatus] --> B{Switch Expression} B -- Pending --> C[Output: "Awaiting confirmation"] B -- Processing --> D[Output: "Being prepared"] B -- Shipped --> E[Output: "Left warehouse"] B -- Delivered --> F[Output: "Successfully delivered"] B -- Cancelled --> G[Output: "Cancelled"] B -- _ (default) --> H[Output: "Unknown status"]
Flow of a switch expression for order status.
Advanced Pattern Matching: Type, Property, and Positional Patterns (C# 7.0+)
Pattern matching is a powerful feature that allows you to test an expression against various patterns. This goes far beyond simple equality checks, enabling you to check types, properties, and even deconstruct objects directly within your conditional logic. This dramatically reduces the need for explicit if
checks and casts.
Type Patterns
Type patterns allow you to check the type of an object and cast it to a new variable in one go. This is incredibly useful for handling polymorphic types.
public abstract class Shape { }
public class Circle : Shape { public double Radius { get; set; } }
public class Rectangle : Shape { public double Length { get; set; } public double Width { get; set; } }
public double CalculateArea(Shape shape) =>
shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Length * r.Width,
_ => throw new ArgumentException("Unknown shape type")
};
Using type patterns in a switch expression to calculate area.
Property Patterns
Property patterns allow you to match an object based on the values of its properties. This is particularly useful for complex business rules.
public class Product
{
public string Category { get; set; }
public decimal Price { get; set; }
public bool IsDiscounted { get; set; }
}
public decimal GetDiscountedPrice(Product product) =>
product switch
{
{ Category: "Electronics", Price: > 1000 } => product.Price * 0.90m, // 10% off expensive electronics
{ Category: "Books", IsDiscounted: true } => product.Price * 0.75m, // 25% off discounted books
{ Price: < 50 } => product.Price * 0.95m, // 5% off cheap items
_ => product.Price
};
Applying property patterns to determine product discounts.
Positional Patterns (C# 8.0+)
Positional patterns deconstruct an object into its constituent parts and match against those parts. This requires the object to have a Deconstruct
method.
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
public string GetQuadrant(Point p) =>
p switch
{
(0, 0) => "Origin",
(int x, int y) when x > 0 && y > 0 => "Quadrant I",
(int x, int y) when x < 0 && y > 0 => "Quadrant II",
(int x, int y) when x < 0 && y < 0 => "Quadrant III",
(int x, int y) when x > 0 && y < 0 => "Quadrant IV",
(_, 0) => "On X-axis",
(0, _) => "On Y-axis",
_ => "Unknown"
};
Using positional patterns with a Point
class to determine its quadrant.
when
clause allows adding extra conditions to any pattern.When to Use Which Approach
Choosing the right approach depends on the complexity and nature of your conditional logic. Here's a general guideline:
1. Simple Enum or Primitive Value Checks
For basic equality checks against enums, integers, or strings, a switch
expression is often the most concise and readable choice.
2. Type-Based Dispatch
When you need to perform different actions based on the runtime type of an object, type patterns within a switch
expression are ideal. This is a common scenario in polymorphic designs.
3. Complex Object State or Multiple Conditions
For scenarios where you need to check multiple properties of an object, or a combination of type and property values, property patterns offer a highly expressive solution. Use when
clauses for additional filtering.
4. Deconstructing Objects
If your object has a Deconstruct
method and you want to match based on its constituent parts, positional patterns are the way to go.
5. Legacy Code or Simple Cases
The traditional switch
statement is still perfectly acceptable for very simple, non-evolving cases, or when working with older C# versions.