Pattern Matching in C#: Embracing Functional Programming

Abhishek Malaviya
6 min readJul 1, 2024

--

Pattern matching in C# is a powerful feature that makes your code more expressive and readable. It allows you to check if an object matches a specific pattern and extract values from it if it does.

C# supports multiple patterns, including declaration, type, constant, relational, property, list, var, and discard. These patterns can be combined using Boolean logic keywords such as and, or, and not.

It is a functional programming technique, which means it emphasizes the evaluation of expressions rather than the control flow of your code.

Following are some common use cases with examples to show how pattern matching can be effectively used in C#:

Switch Expression

Pattern matching in switch expressions can make your code more concise and expressive.

string GetInfoByType(object obj) => obj switch
{
string s => $"A string of length {s.Length}",
int i => $"An integer with value {i}",
null => "A null object",
_ => "An unknown type"
};

Here we are using discard _ pattern for the default switch condition.

Output

Logical Patterns

You can combine multiple patterns using logical operators say for example the not, and, and or pattern combinators to create the logical patterns.

Here we are performing null checks and range checks using a combination of not and and patterns.

int ? number = 510;

if (number is not null and > 100 and < 1000)
{
Console.WriteLine("This is a three digit positive number.");
}

The following example combine both and and or patterns.

static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

Output

Type Checking and Casting

Pattern matching can be used to check the type of an object and cast it simultaneously, making it a developer-friendly feature.

object obj = "Hello, world!";
if (obj is string s)
{
Console.WriteLine($"String length: {s.Length}");
}

Parenthesized pattern

You can put parentheses around any pattern to emphasize or change the precedence in logical patterns, as the following example shows:

if (result is not (float or double))
{
return;
}

Property Pattern

Property pattern matching can be utilized to match and extract properties from objects using an easy-to-understand syntax. We can see how powerful this approach is.

Employee employee = new("Mike", 28);

if (employee is { Name: "Mike", Age: var age })
{
Console.WriteLine($"{employee.Name} is {age} years old.");
}

Console.ReadLine();

public class Employee
{
public string Name { get; set; }
public int Age { get; set; }

public Employee(string name, int age)
{
Name = name;
Age = age;
}
}

Output

Constant pattern

You use a constant pattern to test if an expression result equals a specified constant, as the following example shows:

public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
1 => 12.0m,
2 => 20.0m,
3 => 27.0m,
4 => 32.0m,
0 => 0.0m,
_ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
};

The expression must be a type that is convertible to the constant type.

In a constant pattern, you can use any constant expression, such as:

  • An integer or floating-point numerical literal
  • A char
  • A string literal
  • A Boolean value (true or false)
  • An enum value
  • The name of a declared const field or local
  • null

List patterns

You can check elements in a list or an array using a list pattern. A list pattern allows you to apply a pattern to each element of a sequence. Additionally, you can use the discard pattern _to match any element, or apply a slice pattern ..to match zero or more elements.

A slice pattern ..matches zero or more elements. You can use at most one slice pattern in a list pattern. The slice pattern can only appear in a list pattern.

List patterns are a valuable tool when data doesn’t follow a regular structure.

// Discard Pattern
var colors = new[] { "red", "blue", "green" };
if (colors is ["red", _, _])
{
Console.WriteLine("The colors are matched.");
}

// Slice Pattern
var evenNumbers = new[] { 10, 20, 30, 40, 50 };
if (evenNumbers is [10, ..])
{
Console.WriteLine("Numbers are even.");
}

Console.ReadLine();

Output

Multiple inputs

Generally, we create conditions for a single input, but we can also use pattern matching to create conditions based on multiple inputs.

You can write patterns that examine multiple properties of an object.
Consider the following Order record, where we calculate the discount based on the quantity and cost properties:

Order order = new Order(Guid.NewGuid(), 15, 1200);

Console.WriteLine($"This order has a discount: {CalculateDiscount(order)}");
Console.ReadLine();

decimal CalculateDiscount(Order order) =>
order switch
{
{ Quantity: > 10, Cost: > 1000.00m } => 0.10m,
{ Quantity: > 5, Cost: > 500.00m } => 0.05m,
{ Cost: > 250 } => 0.02m,
null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
};

public record Order(Guid ItemID, int Quantity, decimal Cost);

Output

Relational patterns

In a relational pattern, you can use any of the relational operators <, >, <=, or >=. The right-hand part of a relational pattern must be a constant expression, which can be an integer, floating-point, char, or enum type.

Here we are defining calendar seasons based on month ranges throughout the year. In a switch expression, the right-hand side season is a constant value.

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14)));  // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19))); // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17))); // output: winter

static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and < 3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

Deconstructing Tuples

You can use the names of tuple elements and Deconstruct parameters in a positional pattern, as the following example shows:

var numbers = new List<int> { 10, 25, 38 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
Console.WriteLine($"Sum of numbers [{string.Join(" ", numbers)}] is {sum}");
}

static (int Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
int sum = 0;
int count = 0;
foreach (int number in numbers)
{
sum += number;
count++;
}
return (sum, count);
}

You can deconstruct tuples and use pattern matching to process individual elements. Here, we are deconstructing the sum and count elements of a tuple and ensuring the count is not zero.

Output

Conclusion

Pattern matching in C# provides a powerful and expressive way to handle various types and structures in your code, enabling you to write more readable and maintainable code. Patterns can be used to express complex conditions in a concise and cleaner way.

Thanks for reading!

--

--