Luna Tech | Dive into C# Pattern Matching

May 29, 2020 字数 2416 12 min

Slide in HTML version: https://bit.ly/3etbRRH


Agenda

  1. Introduction

  2. Pattern Matching in C# 7

  3. Pattern Matching in C# 8

  4. Pattern Matching in C# 9

  5. Wrap up


Pattern Matching

1. What is Pattern Matching?

Pattern matching is a feature that allows you to implement method on both object properties and object type.

2. Pattern Matching in C# 7.0

  • The “is” Pattern

  • “switch” supports Type Pattern

  • The “when” Pattern

  • Cases can be grouped


C# 7.0: The “is” pattern

1
2
3
4
5
6
7
var input = 1;
var sum = 0;

if (input is int count)
    sum += count;

Console.WriteLine(sum); // 1

C# 7.0: The “is” pattern - Example

Before

1
2
if (abc is UserService)
    ((UserService)abc).IdentityService = this;

After

1
2
if (abc is UserService userService)
    userService.IdentityService = this;

Before C# 7.0: “switch” with Constant Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Basic switch syntax, only support constant pattern
public static string BasicSwitch(params string[] parts) {
    switch (parts.Length)
    {
        case 0:
            return "No elements to the input";
        case 1:
            return $"One element: {parts[0]}";
        default:
            return $"Many elements. Too many to write";
    }
}

C# 7.0: “switch” supports Type Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Basic switch syntax, only support constant pattern
public static string BasicSwitch(params string[] parts) {
    switch (parts.Length)
    {
        case 0:
            return "No elements to the input";
        case 1:
            return $"One element: {parts[0]}";
        default:
            return $"Many elements. Too many to write";
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// From C# 7.0, "switch" starts to support type pattern
public static double ComputeAreaModernSwitch(object shape) {
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        default:
            return -1;
    }
}

C# 7.0: The “when” Pattern

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static double ComputeAreaCaseWhen(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
            return 0;
        case Triangle t when t.Base == 0 || t.Height == 0:
            return 0;
        case Square s:
            return s.Side * s.Side;
        case null: // a special case for null handling
            throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
        default: // won't be null
            return -1;
    }
}

C# 7.0: Cases can be grouped

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static double ComputeAreaCaseWhen(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
            return 0;
        case Square s:
            return s.Side * s.Side;
        case null: // a special case for null handling
            throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
        default: // won't be null
            return -1;
    }
}

What’s new in C# 8.0?

Pattern Matching in C# 8.0

  1. Switch expression

  2. Positional pattern

  3. Property pattern

  4. Tuple pattern


C# 8.0: Switch Expression

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public enum Color { Red, Blue, Yellow }

// Before
public static string GetColor(Color c) {
    switch (c)
    {
        case Color.Red:
            return "Red";
        case Color.Blue:
            return "Blue";
        case Color.Yellow:
            return "Yellow";
        default:
            return "Invalid color";
    }
}

// After
public static string GetColor(Color c) => c switch
    {
        Color.Red    => "Red",
        Color.Blue   => "Blue",
        Color.Yellow => "Yellow",
        _            => "Invalid Color" 
    };

Syntax improvements in Switch Expression

1
2
3
4
5
6
7
public static string GetColor(Color c) => c switch // 1.
    {
        Color.Red    => "Red", // 2. 4.
        Color.Blue   => "Blue",
        Color.Yellow => "Yellow",
        _            => "Invalid Color" // 3.
    };
  1. variable comes before the switch keyword

  2. The case and : elements are replaced with =>.

  3. The default case is replaced with a _ discard.

  4. The bodies are expressions (e.g., "Red"), not statements (e.g., return "Red").


Side Note 1: _ Discards

  1. A discard is a write-only variable.

  2. You can assign all of the values that you intend to discard to the single variable.

  3. Discards can be used in:

    • Tuple and object deconstruction.

    • Calls to methods with out parameters.

    • Pattern matching with is and switch.

    • A standalone _ when no _ is in scope.


Side Note 2: out parameter modifier

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void Method(out int answer, out string message, out string stillNull)
{
    answer = 44;
    message = "I've been returned";
    stillNull = null;
}

int argNumber;
string argMessage, argDefault;

Method(out argNumber, out argMessage, out argDefault);

Console.WriteLine(argNumber); // 44
Console.WriteLine(argMessage); // I've been returned
Console.WriteLine(argDefault == null); // True

out variable in C# 7.0

From C# 7.0, we don’t need to declare a variable separately.


out variable in C# 7.0 - Example

Before

1
2
3
4
5
6
7
public IEnumerable<Dictionary<string, object>> GetPlugins() {
    object pluginsObj = null;
    bool isConfigured = _serviceConf.TryGetValue("Plugins", out pluginsObj);

    // ... code omitted
    return Enumerable.Empty<Dictionary<string, object>>();
}

After

1
2
3
4
5
6
public IEnumerable<Dictionary<string, object>> GetPlugins() {
    bool isConfigured = _serviceConf.TryGetValue("Plugins", out object pluginsObj);

    // ... code omitted
    return Enumerable.Empty<Dictionary<string, object>>();
}

Side Note 3: Deconstruct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x1, int y1)
        => (X, Y) = (x1, y1);

    public void Deconstruct(out int x2, out int y2) =>
        (x2, y2) = (X, Y);
}
1
2
3
4
5
6
var p = new Point(2, 5);

// Choose your flavor
(int a1, int b1) = p;
var (a3, b3) = p;
(var a4, var b4) = p;

*When a Deconstruct method is accessible, you can use positional patterns to inspect properties of the object and use those properties for a pattern.*


Pattern 2: Positional Pattern

https://dotnetfiddle.net/T5dzd3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// C# 7.0
static string Display(object o)
{
    switch (o)
    {
        case Point p when p.X == 0 && p.Y == 0:
            return "origin";
        case Point p:
            return $"({p.X}, {p.Y})";
        default:
            return "unknown";
    }
}
1
2
3
4
5
6
7
// C# 8.0
static string DisplayWithPositionalPatterns(object o) => o switch
{
    Point(0, 0) => "origin",
    Point(var x, var y) => $"({x}, {y})",
    _ => "unknown"
};

Side Note 4: Expression-bodied function

  • Expression-bodied function members is introduced in C# 6.0 for methods and read-only properties.

  • Many members that you write are single statements that could be single expressions, you can turn it into an expression-bodied member.

  • From C# 7.0, you can also implement constructors, finalizers (destructors), and get and set accessors on properties and indexers.

Example

1
2
3
4
5
6
7
// Before: a single-statement method
public string GetName() { 
    return $"{FirstName} {LastName}"; 
}

// After: implement Expression-bodied function 
public string GetName() => $"{FirstName} {LastName}";

Expression-bodied function example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// method
public string GetName() => $"{FirstName} {LastName}";

// read-only property
public readonly string FullName => $"{FirstName} {LastName}";

// constructor
public ExpressionMembersExample(string label) => this.Label = label;

// finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

// Expression-bodied get / set accessors.
private string _fullname;
public string FullName
{
    get => _fullname;
    set => this._fullname = value ?? "Default name";
}

Exercise: rewrite the code (1)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// The original code to be simplified
public string UserService
{
    get
    {
		object obj = null;

    	if (_serviceConf.TryGetValue("User", out obj))
    		return obj?.ToString();

    	return null;
    }
}

Exercise: rewrite the code (2)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// use out parameter
public string UserService
{
    get
    {
    	if (_serviceConf.TryGetValue("User", out object obj))
    		return obj?.ToString();
    	return null;
    }
}

Exercise: rewrite the code (3)

1
2
3
4
5
6
7
8
// use ternary operator
public string UserService
{
    get
    {
    	return _serviceConf.TryGetValue("User", out object obj) ? obj?.ToString() : null;
    }
}

Exercise: rewrite the code (4)

1
2
3
4
5
// use expression-bodied function
public string UserService
{
	get => _serviceConf.TryGetValue("User", out object obj) ? obj?.ToString() : null;
}

Exercise: rewrite the code (5)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Before
public string UserService
{
    get
    {
		object obj = null;

    	if (_serviceConf.TryGetValue("User", out obj))
    		return obj?.ToString();

    	return null;
    }
}
1
2
3
4
5
// After
public string UserService
{
	get => _serviceConf.TryGetValue("User", out object obj) ? obj?.ToString() : null;
}

Pattern 3: Property Pattern

The property pattern enables you to match on properties of the object examined.

BUT, Deconstruct method is not required for the object type.

Now let’s use C# 8.0 Property Pattern to rewrite the same query in the positional pattern example.


Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static string Display(object o)
	{
		switch (o)
		{
			case Point p when p.X == 0 && p.Y == 0:
				return "origin";
			case Point p:
				return $"({p.X}, {p.Y})";
			default:
				return "unknown";
		}
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static string DisplayWithPropertyPatterns1(object o) => o switch
{
    Point { X: 0, Y: 0 }         p => "origin",
    Point { X: var x, Y: var y } p => $"({x}, {y})",
    _                              => "unknown"
};

static string DisplayWithPropertyPatterns2(object o) => o switch
{
    Point { X: 0, Y: 0 }         => "origin",
    Point { X: var x, Y: var y } => $"({x}, {y})",
    {}                           => o.ToString(), // {} = "not-null" pattern
    null                         => "null"
};

Technique: Switch within switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static string DisplayShapeInfoWithSwitchExpression(object shape) => shape switch
{
    Rectangle r => r switch
    {
        _ when r.Length == r.Width => "Square!",
        _ => "",
    },
    Circle { Radius: 1 } c => $"Small Circle!",
    Circle c => $"Circle (r={c.Radius})",
    Triangle t => $"Triangle ({t.Side1}, {t.Side2}, {t.Side3})",
    _ => "Unknown Shape"
};

Side Note 5: Tuples

  1. C# tuples are types that you define using a lightweight syntax.

  2. Basic version introduced before C# 7.0, improved a lot in C# 7.0 release

  3. One of the most common uses for tuples is as a method return value.


Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// create a tuple without semantic property names (unnamed tuple)
var tuple = (1, 2);
Console.WriteLine($"{tuple.Item1}, {tuple.Item2}");

// create tuple1 with semantic property names (named tuple)
(string first, string second) tuple1 = ("a", "b");
Console.WriteLine($"{tuple1.first}, {tuple1.second}");

// create tuple2 with semantic property names (right-hand side assignment)
var tuple2 = (first: "a", second: "b");
Console.WriteLine($"{tuple2.first}, {tuple2.second}");

// deconstruct tuple2
var (a, b) = tuple2;
Console.WriteLine($"{a}, {b}");

// tuple as return value
static (double, string, int) TupleAsReturnValue() => (2.2, "abc", 1);

Pattern 4: Tuple Pattern

Tuple patterns allow you to switch based on multiple values expressed as a tuple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public enum Color { Unknown, Red, Blue, Yellow }

static Color GetColor(Color c1, Color c2) => (c1, c2) switch
{
    (Color.Red, Color.Blue) => Color.Purple,
    (Color.Blue, Color.Red) => Color.Purple,
    (Color.Yellow, Color.Red) => Color.Orange,
    (Color.Red, Color.Yellow) => Color.Orange,

    (_, _) when c1 == c2 => c1,
    _ => Color.Unknown
};
1
2
3
4
5
6
7
8
static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition) switch
{
    (Opened, Close)              => Closed,
    (Closed, Open)               => Opened,
    (Closed, Lock)   when hasKey => Locked,
    (Locked, Unlock) when hasKey => Closed,
    _ => throw new InvalidOperationException($"Invalid transition")
};

Future: What’s new in C# 9.0?

C# 9.0 Pattern Matching Improvements

  1. Simple type patterns
  2. Relational patterns
  3. Logical patterns

C# 9.0: Simple Type Pattern

If a type pattern identifier is a discard, we can omit the _ identifier, just use type alone is fine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// C# 8.0
static string DisplayWithPropertyPatterns1(object o) => o switch
    {
        Point { X: 0, Y: 0 }  p => "origin",
        Point _                 => "Not an original point",
        _                       => "Not a point"
    };

// C# 9.0
static string DisplayWithPropertyPatterns2(object o) => o switch
    {
        Point { X: 0, Y: 0 }  p => "origin",
        Point                   => "Not an original point",
        _                       => "Not a point"
    };

C# 9.0: Relational patterns

C# 9.0 introduces patterns corresponding to the relational operators <, <= and so on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// C# 8.0
public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
        DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
        DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
        DeliveryTruck _ => 10.00m,

        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };

// C# 9.0
public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
        DeliveryTruck t when t.GrossWeightClass switch
        {
            // Here > 5000 and < 3000 are relational patterns.
            > 5000 => 10.00m + 5.00m,
            < 3000 => 10.00m - 2.00m,
            _ => 10.00m,
        },
        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };

C# 9.0: Logical patterns

You can combine patterns with logical operators and, or and not instead of &&, ||, !.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// C# 9.0
public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
        DeliveryTruck t when t.GrossWeightClass switch
        {
            < 3000 => 10.00m - 2.00m,
            // a pattern representing an interval.
            >= 3000 and <= 5000 => 10.00m,
            > 5000 => 10.00m + 5.00m,
        },
        // not pattern can be applied to null to handle unknown cases
        not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
        null => throw new ArgumentNullException(nameof(vehicle))
    };

// Use not in if-conditions
if (!(e is Customer)) { ... }
if (e is not Customer) { ... }

Wrap up

  1. Pattern Matching

    • C# 7 (is, when)

    • C# 8 (switch express, positional pattern, property pattern, tuple pattern)

    • C# 9 (Simple type pattern, Relational pattern, Logical pattern)

  2. Other important concepts

    • Discards

    • Deconstruct

    • out parameter

    • Expression-bodied function

    • Tuples


Thanks for listening / reading :)


References

  1. Welcome to C# 9.0
  2. Pattern Matching
  3. Tuples
  4. C# Expressions, Statements and Blocks
  5. Statements
  6. Discards
  7. Out Parameter Modifier

Talk to Luna


Support Luna