DEV Community

Cover image for Pattern Matching in C#: The Starters Guide
Rasheed K Mozaffar
Rasheed K Mozaffar

Posted on

Pattern Matching in C#: The Starters Guide

Hey there! 👋🏻

If you've been coding in C# for a while, you've probably used the traditional if-else statements and the classic switch statement to handle different conditions in your code. But did you know that C# has evolved significantly in this area? Pattern matching is one of those powerful features that makes your code cleaner, more readable, and honestly, more fun to write!

In this post, we're going to explore pattern matching in C#, and I'll show you how it can transform your conditional logic from verbose and repetitive to concise and elegant.

What is Pattern Matching? 🤔

Pattern matching is a feature in C# that allows you to test if a value has a certain "shape" and extract information from it at the same time. Instead of writing multiple if statements to check types, values, or properties, you can express these checks more naturally and in fewer lines of code.

Think of it as a smarter, more expressive way to ask questions about your data.

The Classic Switch Statement (The Old Way) 🕰️

Before we dive into pattern matching, let's look at how we used to handle different cases with the traditional switch statement:

public string GetAnimalSound(string animal)
{
    switch (animal)
    {
        case "Dog":
            return "Woof!";
        case "Cat":
            return "Meow!";
        case "Cow":
            return "Moo!";
        default:
            return "Unknown sound";
    }
}
Enter fullscreen mode Exit fullscreen mode

This works, but it's quite verbose, especially when you need to handle more complex scenarios. Now let's see how pattern matching makes this better!

Switch Expressions (The New Way) ✨

C# introduced switch expressions, which are a more concise way to write switch statements. Here's the same code rewritten using a switch expression:

public string GetAnimalSound(string animal) => animal switch
{
    "Dog" => "Woof!",
    "Cat" => "Meow!",
    "Cow" => "Moo!",
    _ => "Unknown sound"
};
Enter fullscreen mode Exit fullscreen mode

Much cleaner, right? Notice how we got rid of the case, return, and break keywords. The _ at the end is the discard pattern, which acts like the default case.

Type Patterns 🎯

One of the most useful aspects of pattern matching is checking the type of an object and extracting it in one go. Before pattern matching, you'd have to do something like this:

// The old way
public void ProcessPayment(object payment)
{
    if (payment is CreditCard)
    {
        var creditCard = (CreditCard)payment;
        Console.WriteLine($"Processing credit card ending in {creditCard.LastFourDigits}");
    }
    else if (payment is PayPal)
    {
        var paypal = (PayPal)payment;
        Console.WriteLine($"Processing PayPal payment for {paypal.Email}");
    }
}
Enter fullscreen mode Exit fullscreen mode

With pattern matching, you can check the type and declare a variable in one line:

// The new way
public void ProcessPayment(object payment)
{
    if (payment is CreditCard card)
    {
        Console.WriteLine($"Processing credit card ending in {card.LastFourDigits}");
    }
    else if (payment is PayPal paypal)
    {
        Console.WriteLine($"Processing PayPal payment for {paypal.Email}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Even better, you can use a switch expression for this:

public string ProcessPayment(object payment) => payment switch
{
    CreditCard card => $"Processing credit card ending in {card.LastFourDigits}",
    PayPal paypal => $"Processing PayPal payment for {paypal.Email}",
    BankTransfer bank => $"Processing bank transfer from {bank.AccountNumber}",
    _ => "Unknown payment method"
};
Enter fullscreen mode Exit fullscreen mode

Property Patterns 🔍

Property patterns let you match based on the properties of an object. This is incredibly useful when you want to make decisions based on the state of an object.

Let's say we have a Person class and we want to determine if someone is eligible for a senior discount:

public record Person(string Name, int Age, string Country);

public bool IsEligibleForSeniorDiscount(Person person) => person switch
{
    { Age: >= 65, Country: "USA" } => true,
    { Age: >= 60, Country: "UK" } => true,
    { Age: >= 67, Country: "Germany" } => true,
    _ => false
};
Enter fullscreen mode Exit fullscreen mode

You can even combine multiple conditions:

public string GetTicketPrice(Person person) => person switch
{
    { Age: < 12 } => "Child ticket: $5",
    { Age: >= 12 and < 18 } => "Teen ticket: $8",
    { Age: >= 18 and < 65 } => "Adult ticket: $12",
    { Age: >= 65 } => "Senior ticket: $6",
    _ => "Unknown"
};
Enter fullscreen mode Exit fullscreen mode

Relational Patterns 📊

Relational patterns allow you to use comparison operators like <, >, <=, and >= directly in your patterns. This makes range checking much cleaner:

public string GetGrade(int score) => score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};
Enter fullscreen mode Exit fullscreen mode

Compare this to the traditional approach:

// The old way
public string GetGrade(int score)
{
    if (score >= 90)
        return "A";
    else if (score >= 80)
        return "B";
    else if (score >= 70)
        return "C";
    else if (score >= 60)
        return "D";
    else
        return "F";
}
Enter fullscreen mode Exit fullscreen mode

The pattern matching version is not only shorter but also more readable!

Logical Patterns (and, or, not) 🧮

You can combine patterns using logical operators to create more complex conditions:

public string CheckWeather(int temperature) => temperature switch
{
    < 0 => "Freezing!",
    >= 0 and < 15 => "Cold",
    >= 15 and < 25 => "Mild",
    >= 25 and < 35 => "Warm",
    >= 35 => "Hot!",
    _ => "Unknown"
};

public bool IsWeekend(DayOfWeek day) => day switch
{
    DayOfWeek.Saturday or DayOfWeek.Sunday => true,
    _ => false
};

public bool IsValidAge(int age) => age switch
{
    >= 0 and <= 120 => true,
    _ => false
};
Enter fullscreen mode Exit fullscreen mode

Real-World Example 🌍

Let's put it all together with a practical example. Imagine you're building an e-commerce system and need to calculate shipping costs based on multiple factors:

public record Order(decimal TotalAmount, string Country, bool IsPrime, int Weight);

public decimal CalculateShipping(Order order) => order switch
{
    { IsPrime: true } => 0m,
    { Country: "USA", Weight: <= 1 } => 5m,
    { Country: "USA", Weight: > 1 and <= 5 } => 10m,
    { Country: "USA", Weight: > 5 } => 15m,
    { Country: "Canada", Weight: <= 1 } => 8m,
    { Country: "Canada", Weight: > 1 and <= 5 } => 15m,
    { Country: "Canada", Weight: > 5 } => 25m,
    { TotalAmount: >= 100 } => 0m,
    _ => 20m
};
Enter fullscreen mode Exit fullscreen mode

This code is clean, easy to read, and handles multiple conditions elegantly. Try writing the same logic with traditional if-else statements, and you'll see how much more verbose it would be!

When to Use Pattern Matching ⚙️

Pattern matching shines when you need to:

  • Check types and extract values at the same time
  • Make decisions based on object properties
  • Handle multiple conditions with different ranges or values
  • Replace long chains of if-else statements
  • Make your code more expressive and readable

Conclusion ✅

Pattern matching in C# is a powerful feature that makes your code more concise and expressive. We've covered switch expressions, type patterns, property patterns, relational patterns, and logical patterns. Each of these gives you a new tool to write cleaner, more maintainable code.

The best way to get comfortable with pattern matching is to start using it in your projects. Next time you're writing a switch statement or a long chain of if-else conditions, try refactoring it with pattern matching and see the difference!

I hope you learned something new from this post!

Thanks for reading! 👋🏻

Top comments (0)