C# Pattern Matching
Pattern matching is one of C#'s most powerful features that allows you to test expressions against certain patterns. It enables you to write more expressive and concise code by combining type checking, value testing, and variable assignment in a single operation.
Introduction to Pattern Matching
Pattern matching in C# lets you check if a value matches a certain pattern and extract information from it when it does. This feature has evolved considerably since its introduction in C# 7.0 and has become increasingly sophisticated with each new C# version.
Before pattern matching, checking types and extracting data often required verbose code with explicit type checking, casting, and conditional logic. Pattern matching simplifies this process significantly.
Basic Pattern Matching Types
Let's explore the different kinds of pattern matching available in C#:
Type Patterns
The most basic form of pattern matching is the type pattern, which checks if an object is of a specific type.
public static string GetShapeDescription(object shape)
{
if (shape is Circle)
{
// Traditional approach
Circle circle = (Circle)shape;
return $"Circle with radius {circle.Radius}";
}
// Using type pattern
if (shape is Rectangle rectangle)
{
return $"Rectangle with width {rectangle.Width} and height {rectangle.Height}";
}
return "Unknown shape";
}
The is
operator checks if shape
is a Rectangle
and if so, assigns it to the rectangle
variable in one step.
Constant Patterns
Constant patterns check if a value matches a specific constant:
public static string CheckValue(object value)
{
if (value is null)
{
return "Value is null";
}
if (value is 100)
{
return "Value is 100";
}
if (value is "hello")
{
return "Value is the string 'hello'";
}
return "Value is something else";
}
Property Patterns
Introduced in C# 8.0, property patterns allow you to match against properties of an object:
public static bool IsRedCircle(Shape shape)
{
return shape is Circle { Color: "Red" };
}
public static bool IsLargeSquare(Shape shape)
{
return shape is Square { SideLength: > 10 };
}
Tuple Patterns
You can match against the components of a tuple:
public static string DescribePoint(Point point)
{
return (point.X, point.Y) switch
{
(0, 0) => "Origin",
(0, _) => "On Y-axis",
(_, 0) => "On X-axis",
(_, _) => "Regular point"
};
}
Advanced Pattern Matching
Switch Expressions (C# 8.0+)
Switch expressions provide a more concise syntax for pattern matching compared to the traditional switch statement:
public static decimal CalculateDiscount(Customer customer) => customer switch
{
{ LoyaltyYears: > 5, PurchasesThisMonth: > 10 } => 0.20m,
{ LoyaltyYears: > 5 } => 0.10m,
{ PurchasesThisMonth: > 10 } => 0.05m,
_ => 0m
};
Relational Patterns (C# 9.0+)
Relational patterns allow you to compare values using relational operators:
public static string GetTemperatureDescription(int temperature) => temperature switch
{
< 0 => "Freezing",
>= 0 and < 20 => "Cool",
>= 20 and < 30 => "Warm",
>= 30 => "Hot"
};
Logical Patterns (C# 9.0+)
You can combine patterns using logical operators:
public static bool IsValidInput(string input) => input is not null and not "";
public static string CheckNumber(int number) => number switch
{
< 0 => "Negative",
> 0 and < 10 => "Small positive",
>= 10 and < 100 => "Medium positive",
>= 100 or 0 => "Large positive or zero"
};
Real-world Applications
Parsing Command Arguments
Pattern matching is excellent for parsing command-line arguments:
public static void HandleCommand(string[] args)
{
switch (args)
{
case ["help"] or ["-h"] or ["--help"]:
DisplayHelp();
break;
case ["create", var name]:
CreateItem(name);
break;
case ["delete", var id, "force"]:
DeleteItem(id, force: true);
break;
case ["delete", var id]:
DeleteItem(id, force: false);
break;
default:
Console.WriteLine("Unknown command. Use 'help' for assistance.");
break;
}
}
Data Validation
Pattern matching makes data validation more expressive:
public static ValidationResult ValidateOrder(Order order)
{
return order switch
{
{ Customer: null } => new ValidationResult(false, "Customer is required"),
{ Items: null or { Count: 0 } } => new ValidationResult(false, "Order must have items"),
{ Items: var items, TotalAmount: var total } when items.Sum(i => i.Price) != total =>
new ValidationResult(false, "Total amount doesn't match sum of item prices"),
{ DeliveryDate: var date } when date < DateTime.Now =>
new ValidationResult(false, "Delivery date cannot be in the past"),
_ => new ValidationResult(true, "Order is valid")
};
}
State Machine Implementation
Pattern matching is powerful for implementing state machines:
public enum UserAction { Login, Logout, Purchase, AddToCart, RemoveFromCart }
public class UserSession
{
public UserState State { get; private set; } = UserState.Anonymous;
public UserState ProcessAction(UserAction action) => (State, action) switch
{
(UserState.Anonymous, UserAction.Login) => UserState.LoggedIn,
(UserState.LoggedIn, UserAction.Logout) => UserState.Anonymous,
(UserState.LoggedIn, UserAction.AddToCart) => UserState.ShoppingCart,
(UserState.ShoppingCart, UserAction.RemoveFromCart) when CartIsEmpty() => UserState.LoggedIn,
(UserState.ShoppingCart, UserAction.Purchase) => UserState.Checkout,
(UserState.Checkout, UserAction.Purchase) => UserState.OrderCompleted,
_ => State // Default case: state remains unchanged
};
private bool CartIsEmpty() => true; // Simplified for this example
}
public enum UserState { Anonymous, LoggedIn, ShoppingCart, Checkout, OrderCompleted }
Complete Example: Shape Processing System
Let's put everything together with a complete example of a shape processing system:
using System;
public abstract class Shape
{
public string Color { get; set; }
public virtual double Area => 0;
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area => Math.PI * Radius * Radius;
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area => Width * Height;
}
public class Square : Rectangle
{
public double SideLength
{
get => Width;
set => Width = Height = value;
}
}
public class Triangle : Shape
{
public double Base { get; set; }
public double Height { get; set; }
public override double Area => 0.5 * Base * Height;
}
public class ShapeProcessor
{
public static string DescribeShape(Shape shape) => shape switch
{
Circle { Radius: <= 0 } or Rectangle { Width: <= 0 } or Rectangle { Height: <= 0 } or Triangle { Base: <= 0 } or Triangle { Height: <= 0 }
=> "Invalid shape with non-positive dimensions",
Circle { Radius: var r } circle =>
$"A {circle.Color} circle with radius {r} and area {circle.Area:F2}",
Rectangle { Width: var w, Height: var h } rectangle when w == h =>
$"A {rectangle.Color} square with side {w} and area {rectangle.Area:F2}",
Rectangle { Width: var w, Height: var h } rectangle =>
$"A {rectangle.Color} rectangle with width {w}, height {h}, and area {rectangle.Area:F2}",
Triangle { Base: var b, Height: var h } triangle =>
$"A {triangle.Color} triangle with base {b}, height {h}, and area {triangle.Area:F2}",
null => "Shape is null",
_ => $"Unknown shape of type {shape.GetType().Name} with area {shape.Area:F2}"
};
public static string CategorizeShapeBySize(Shape shape) => shape?.Area switch
{
null or 0 => "Invalid shape",
<= 10 => "Small shape",
> 10 and <= 100 => "Medium shape",
> 100 => "Large shape"
};
}
// Example usage
static void Main()
{
Shape[] shapes = {
new Circle { Color = "Blue", Radius = 5 },
new Rectangle { Color = "Red", Width = 10, Height = 5 },
new Square { Color = "Green", SideLength = 7 },
new Triangle { Color = "Yellow", Base = 8, Height = 6 },
new Circle { Color = "Purple", Radius = -1 } // Invalid shape
};
foreach (var shape in shapes)
{
Console.WriteLine(ShapeProcessor.DescribeShape(shape));
Console.WriteLine($"Size category: {ShapeProcessor.CategorizeShapeBySize(shape)}");
Console.WriteLine();
}
}
Output:
A Blue circle with radius 5 and area 78.54
Size category: Medium shape
A Red rectangle with width 10, height 5, and area 50.00
Size category: Medium shape
A Green square with side 7 and area 49.00
Size category: Medium shape
A Yellow triangle with base 8, height 6, and area 24.00
Size category: Medium shape
Invalid shape with non-positive dimensions
Size category: Invalid shape
Summary
Pattern matching in C# is a powerful feature that can significantly improve code readability and reduce complexity. We've covered:
- Type patterns for safe type casting and checking
- Constant patterns for matching specific values
- Property patterns for checking object properties
- Relational and logical patterns for more complex comparisons
- Switch expressions for concise pattern matching
- Real-world applications including command parsing, validation, and state machines
As C# continues to evolve, pattern matching capabilities keep expanding, making it an essential tool in every C# developer's toolkit.
Exercises
-
Create a
Vehicle
hierarchy (Car, Truck, Motorcycle) and use pattern matching to calculate registration fees based on vehicle type and properties. -
Implement a simple calculator that uses pattern matching to parse and evaluate expressions like "5 + 3" or "10 * 2".
-
Create a logging system that uses pattern matching to format different types of log entries (Error, Warning, Info) in different ways.
-
Extend the shape example to include more complex shapes and additional pattern matching scenarios.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)