C# Pattern Matching
Pattern matching is a powerful feature in C# that allows you to test whether a value has a certain shape and extract information from it when the pattern matches. Introduced in C# 7.0 and expanded in later versions, pattern matching helps you write cleaner, more expressive code when checking types and conditions.
Introduction to Pattern Matching
At its core, pattern matching lets you check if data matches a specific pattern and extract values from it, all in a single operation. This eliminates the need for multiple checks and casts, resulting in more readable and less error-prone code.
In C#, pattern matching primarily works with:
- The
is
operator switch
statementsswitch
expressions (C# 8.0+)
Let's explore how pattern matching improves the way we write conditional code in C#.
Basic Pattern Matching with the is
Operator
Type Pattern
The most basic form of pattern matching checks if a value is of a specific type.
object value = "Hello, World!";
if (value is string message)
{
// 'message' is now a string variable containing "Hello, World!"
Console.WriteLine($"The string is {message.Length} characters long");
}
Output:
The string is 13 characters long
In this example, value is string message
does three things in one operation:
- Checks if
value
is a string - Casts
value
to string - Assigns the result to a new variable
message
This is much cleaner than the traditional approach:
object value = "Hello, World!";
if (value is string)
{
string message = (string)value;
Console.WriteLine($"The string is {message.Length} characters long");
}
Constant Pattern
You can also match against constant values:
object value = 42;
if (value is 42)
{
Console.WriteLine("The answer to everything!");
}
Output:
The answer to everything!
Relational Pattern (C# 9.0+)
C# 9.0 introduced relational patterns, which allow you to use comparison operators in patterns:
int temperature = 32;
string description = temperature is > 30 ? "Hot" : "Not hot";
Console.WriteLine($"Weather: {description}");
Output:
Weather: Hot
Pattern Matching in Switch Statements
Switch statements become much more powerful with pattern matching:
object item = "Test";
switch (item)
{
case string s when s.Length > 10:
Console.WriteLine("This is a long string");
break;
case string s:
Console.WriteLine($"This is a string: {s}");
break;
case int i when i > 100:
Console.WriteLine("This is a large number");
break;
case int i:
Console.WriteLine($"This is a number: {i}");
break;
case null:
Console.WriteLine("This is null");
break;
default:
Console.WriteLine("This is something else");
break;
}
Output:
This is a string: Test
This example demonstrates several pattern matching features:
- Type patterns (
case string s:
) - Pattern with conditions using
when
- Null pattern (
case null:
)
Switch Expressions (C# 8.0+)
C# 8.0 introduced switch expressions, which provide an even more concise syntax:
object value = 5;
string result = value switch
{
string s when s.Length > 0 => $"Non-empty string: {s}",
string _ => "Empty string",
int i when i < 0 => "Negative number",
int i when i == 0 => "Zero",
int i => $"Positive number: {i}",
null => "Null value",
_ => "Something else"
};
Console.WriteLine(result);
Output:
Positive number: 5
The _
(discard pattern) in the second case matches any string but doesn't assign it to a variable since we don't need its value. The last case _
serves as a default case.
Property Pattern (C# 8.0+)
Property patterns let you match on properties of an object:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
void Describe(Person person)
{
string description = person switch
{
{ Name: "Alice", Age: 30 } => "Alice who is 30",
{ Name: "Alice" } => "Alice who is not 30",
{ Age: > 60 } => "A senior person",
{ Age: >= 20, Name: var name } => $"{name} who is an adult",
{ Age: < 20 } => "A young person",
null => "No person provided",
_ => "Someone else"
};
Console.WriteLine(description);
}
// Testing
Describe(new Person { Name = "Alice", Age = 30 });
Describe(new Person { Name = "Alice", Age = 25 });
Describe(new Person { Name = "Bob", Age = 65 });
Describe(new Person { Name = "Charlie", Age = 25 });
Describe(new Person { Name = "David", Age = 15 });
Output:
Alice who is 30
Alice who is not 30
A senior person
Charlie who is an adult
A young person
Tuple Pattern (C# 8.0+)
Tuple patterns allow matching against multiple values at once:
(string, int) GetWeather() => ("Sunny", 30);
var weather = GetWeather();
string advice = weather switch
{
("Rainy", _) => "Take an umbrella",
("Sunny", > 30) => "Wear sunscreen",
("Sunny", _) => "Enjoy the weather",
("Cloudy", < 20) => "Bring a jacket",
_ => "Check the forecast"
};
Console.WriteLine(advice);
Output:
Enjoy the weather
List Pattern (C# 11.0+)
In C# 11, Microsoft introduced list patterns that allow you to match elements inside lists and arrays:
int[] numbers = { 1, 2, 3 };
string result = numbers switch
{
[1, 2, 3] => "Contains 1, 2, 3 exactly",
[1, 2, ..] => "Starts with 1, 2",
[.., 3] => "Ends with 3",
[] => "Empty array",
_ => "Something else"
};
Console.WriteLine(result);
Output:
Contains 1, 2, 3 exactly
The ..
is a slice pattern that matches any sequence of elements.
Real-World Application Example
Let's look at a practical example of how pattern matching can improve real-world code. Suppose we're building a simple shape calculation system:
public abstract class Shape { }
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
}
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height) => (Width, Height) = (width, height);
}
public class Triangle : Shape
{
public double Base { get; }
public double Height { get; }
public Triangle(double @base, double height) => (Base, Height) = (@base, height);
}
public static double CalculateArea(Shape shape)
{
return shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
null => throw new ArgumentNullException(nameof(shape)),
_ => throw new ArgumentException("Unknown shape type", nameof(shape))
};
}
// Usage
var shapes = new Shape[]
{
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
};
foreach (var shape in shapes)
{
Console.WriteLine($"Area: {CalculateArea(shape):F2}");
}
Output:
Area: 78.54
Area: 24.00
Area: 12.00
In this example, pattern matching makes the CalculateArea
method clean and readable. Without pattern matching, we would need multiple if-else
statements with type checking and casting.
Summary
Pattern matching in C# is a powerful feature that helps you write more expressive and concise code when working with types, values, and properties. We've covered:
- Basic pattern matching with the
is
operator - Type patterns, constant patterns, and relational patterns
- Pattern matching in switch statements
- Switch expressions for more concise syntax
- Property patterns for matching object properties
- Tuple patterns for matching multiple values
- List patterns for matching array elements
By mastering pattern matching, you can write more readable, maintainable code with fewer bugs caused by incorrect type casting or complex conditional logic.
Exercises
- Write a function that categorizes different types of vehicles (car, truck, motorcycle, bicycle) using pattern matching.
- Create a validation function that uses pattern matching to check if a user object has valid properties (e.g., non-empty name, age above 18).
- Implement a function that processes different types of media files (audio, video, image) with different properties using pattern matching.
- Use tuple pattern matching to interpret coordinates with different meanings based on their values.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)