Simplifying Code with Pattern Matching in C#

Introduction

Pattern matching is a functional programming feature that already exists in other popular languages such as Scala, Rust, Python, Haskell, Prolog, and many other languages. It is used to provide a way to test expressions for some conditions while testing the types.

It allows you to efficiently and concisely check if a value matches a certain pattern and then execute code based on that match. This feature is particularly useful for working with complex data structures, such as collections, and for simplifying conditional statements.

C# 7 introduced the Pattern Matching.

Benefits - Patterns Matching

  • Type-Testing
  • Nullable-type checking
  • Typecasting and assignment
  • High readability
  • Concise syntax Less convoluted code.

Usages 

Pattern matching can be used in 2 places.

  • 'is' expressions
  • 'switch' expressions

Patterns Matching Types through C# Versions 

C# 7

  • Type Pattern
  • Declaration Pattern
  • Constant Pattern
  • Null Pattern
  • Var Pattern

C# 8

  • Property Pattern
  • Discard Pattern
  • Positional Pattern
  • Discard Pattern

C# 9

  • Type Pattern
  • Relative Pattern
  • Logical Pattern (Combinators)
  • Negated Null Constant Pattern
  • Parenthesized Pattern

C# 10

  • Extended Property Pattern

C# 11

  • List Pattern

Type Pattern

 A type pattern in C# allows you to check whether an object is of a specific type and, if it is, cast it to that type while declaring a new variable.

using System;

public class Program
{
    public static void Main()
    {
        object someObject = "Hello, World!";

        if (someObject is string stringValue)
        {
            // stringValue is now a strongly-typed variable of type string
            Console.WriteLine($"Length of the string: {stringValue.Length}");
        }
        else
        {
            Console.WriteLine("someObject is not a string.");
        }
    }
}

Declaration Pattern

A declaration pattern in C# is a pattern that not only checks if an expression matches a specific pattern but also declares a new variable with the matched value. This pattern is often used in conjunction with is expressions to perform both the pattern matching and variable declaration in a single operation.

Declaration patterns were introduced in C# 7.0 and provided a convenient way to simplify code.

using System;

public class Program
{
    public static void Main()
    {
        object someObject = 42;

        if (someObject is int intValue)
        {
            // intValue is now a strongly-typed variable of type int
            Console.WriteLine($"The value is an integer: {intValue}");
        }
        else
        {
            Console.WriteLine("The value is not an integer.");
        }
    }
}

Constant Pattern

Testing versus a constant value which can include int, float, char, string, bool, enum, field declared with const, null

It's often used in switch statements and pattern-matching scenarios to perform different actions based on the value of an expression.

using System;

public class Program
{
    public static void Main()
    {
        object someObject = 42;

        if (someObject is int intValue)
        {
            // intValue is now a strongly-typed variable of type int
            Console.WriteLine($"The value is an integer: {intValue}");
        }
        else
        {
            Console.WriteLine("The value is not an integer.");
        }
    }
}

Null Pattern

  • Check if a reference or nullable type is null.
  • It is particularly useful for safely handling null values and reducing null reference exceptions.
  • Null pattern matching was introduced in C# 9.0 as part of the language's pattern-matching enhancements.

Var Pattern

  • Similar to the type pattern, the var pattern matches an expression and checks for null, as well as assigns a value to the variable.
  • The var type is declared based on the matched expression’s compile-time type.
using System;

public class Program
{
    public static void Main()
    {
        object someObject = (42, "Hello, Tahir!");

        if (someObject is var (number, message) && number is int && message is string)
        {
            Console.WriteLine($"Number: {number}, Message: {message}");
        }
        else
        {
            Console.WriteLine("Pattern match failed.");
        }
    }
}

Property Pattern

  • Property pattern matching in C# is a feature that allows you to match objects based on the values of their properties or fields.
  • It simplifies code by enabling you to specify patterns that involve property values directly in pattern-matching expressions.
  • Property pattern matching was introduced in C# 8.0 and is a powerful way to work with complex data structures.
using System;

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

public class Program
{
    public static void Main()
    {
        Person person = new Person { Name = "Alice", Age = 30 };

        if (person is { Name: "Alice", Age: 30 })
        {
            Console.WriteLine("It's Alice, age 30!");
        }
        else
        {
            Console.WriteLine("It's someone else.");
        }
    }
}

Discard Pattern

  • discard pattern in C# allows you to match a pattern but discard the matched value.
  • It's represented by the underscore (_) and is especially useful when you want to indicate that you're not interested in the value that matches a pattern, which can help improve code clarity and readability. Discard patterns were introduced in C# 7.0.
using System;

public class Program
{
    public static void Main()
    {
        object someObject = "Hello, World!";

        if (someObject is string _)
        {
            Console.WriteLine("The object is a string, but we're not interested in its value.");
        }
        else
        {
            Console.WriteLine("The object is not a string.");
        }
    }
}

Positional Pattern

  • Positional patterns in C# allow you to match objects based on the values of their elements in a specific order, making it easier to work with complex data structures like arrays, tuples, or user-defined types.
  • This feature was introduced in C# 8.0 and provides a concise way to match objects by their elements' positions.
using System;

public class Program
{
    public static void Main()
    {
        Point point = new Point(3, 4);

        string location = DescribePoint(point);

        Console.WriteLine(location);
    }

    public static string DescribePoint(Point point)
    {
        return point switch
        {
            (0, 0) => "The point is at the origin (0, 0).",
            (var x, var y) when x == y => $"The point is on the diagonal at ({x}, {y}).",
            (var x, var y) => $"The point is at ({x}, {y}).",
            _ => "Unknown location."
        };
    }
}

public record Point(int X, int Y);

Tuple Pattern

  • Tuple patterns in C# allow you to match objects against specific tuple structures. This feature simplifies the process of deconstructing and matching tuples in pattern-matching expressions.
  • Tuple patterns were introduced in C# 8.0 and make it easier to work with complex data structures involving tuples.
using System;

public class Program
{
    public static void Main()
    {
        var point = (3, 4);

        string location = DescribePoint(point);

        Console.WriteLine(location);
    }

    public static string DescribePoint((int X, int Y) point)
    {
        return point switch
        {
            (0, 0) => "The point is at the origin (0, 0).",
            (_, 0) => "The point is on the x-axis.",
            (0, _) => "The point is on the y-axis.",
            var (x, y) when x == y => $"The point is on the diagonal at ({x}, {y}).",
            var (x, y) => $"The point is at ({x}, {y}).",
        };
    }
}
var person = ("Alice", 30);

var description = person switch
{
    ("Alice", 30) => "This is Alice, age 30.",
    ("Bob", var age) => $"This is Bob, age {age}.",
    _ => "Unknown person."
};

'Enhanced' Type Pattern

  • You can do type checking in switch expressions without using the discards with each type
public string CheckValueType(object value)=> value switch
{
  int => "integer number",
  decimal => "decimal number",
  double => "double number",
  _ => throw new InvalidNumberException(value)
};

Summary

Pattern matching is a powerful feature in C# that simplifies code by allowing you to match objects and structures based on patterns. Whether you're checking types, values, or even properties, pattern matching makes your code more concise and readable.

Pattern matching in C# streamlines your code, reduces errors, and enhances readability, making it a valuable tool for developers.


Similar Articles