C# patterns

2023-02-09 C# Pattern is switch when Deconstruct record

In C# 7 was introduced interesting operator is that allows to match (and also unpack) various types. In subsequent versions of the language its ability was greatly improved with many other patterns.

Basic idea is like this

if (obj is string)
    Console.WriteLine("obj is string");

It returns true if the supplied item is of specified type. That can be useful if we get an arbitrary object and we would like to detect if it has capabilities we need. At the same time, it can also provide cast to specified type. This is called declaration and type pattern.

if (input is string s)
    Console.WriteLine(s.ToLower());

Later there were introduced constant, relational, positional and property patterns. Also there is new switch expression that allows to pattern match against number of rules with nice checking that all options are properly exhausted

string DescribeStringLength(string str) => 
    str switch
    {
        null => "Null string",
        "Hello" => "Hello string",
        { Length: 0 } => "Empty string",
        { Length: 1 } => "Character",
        { Length: > 1 } => "Longer string",
    };

The example above shows checking against null, constant string and number of options in the Length property.

Here is examples of the syntax

C# Meaning
_ Matches anything
List<int> The item has type List<int>
List<int> list The item has type List<int> and is extracted into list variable
10 The item matches constant 10
< -3.0 The item is smaller than -3.0
>= 3 and < 6 The item is within the interval
[1, 2, 3] The extracted items are 1, 2, and 3
Point { X: 10 } The item is of type Point with property X equals 10
{} The item is non-null
("12345", _, _) First extracted item of three is string "12345"

The deconstruction of an item is by default available in Tuples, Arrays, or records but it is possible to make any class so with method Deconstruct

class City
{
    public string Name { get; }
    public int Area { get; }
    public double Population { get; }

    public City(string name, int area, double population) =>
        (Name, Area, Population) = (name, area, population);

    public void Deconstruct(out string name, out int area, out double population) =>
        (name, area, population) = (Name, Area, Population);
}

This is just an example, it is easier to build record class like this

record class City(string Name, int Area, double Population);

Then it is possible to match it like in this a little contrived example that uses when guard to the pattern

City newYork = new City("New York", Population: 8804190, Area: 1223);
Console.WriteLine($"{newYork.Name} population density: " + newYork switch
{
    var (name, area, population) when population > 100000 => "High",
    var (name, area, population) when population < 50000 => "Medium",
    var (name, area, population) when population < 25000 => "Low",
    _ => "Unknown"
});