Skip to main content

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 statements
  • switch 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.

csharp
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:

  1. Checks if value is a string
  2. Casts value to string
  3. Assigns the result to a new variable message

This is much cleaner than the traditional approach:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
(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:

csharp
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:

csharp
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

  1. Write a function that categorizes different types of vehicles (car, truck, motorcycle, bicycle) using pattern matching.
  2. 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).
  3. Implement a function that processes different types of media files (audio, video, image) with different properties using pattern matching.
  4. 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! :)