Dive into C# Pattern Matching
2020-05-29
Slide in HTML version: https://bit.ly/3etbRRH
Agenda
-
Introduction
-
Pattern Matching in C# 7
-
Pattern Matching in C# 8
-
Pattern Matching in C# 9
-
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
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
if (abc is UserService)
((UserService)abc).IdentityService = this;
After
if (abc is UserService userService)
userService.IdentityService = this;
Before C# 7.0: “switch” with Constant Pattern
// 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
// 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";
}
}
// 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
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
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
-
Switch expression
-
Positional pattern
-
Property pattern
-
Tuple pattern
C# 8.0: Switch Expression
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
public static string GetColor(Color c) => c switch // 1.
{
Color.Red => "Red", // 2. 4.
Color.Blue => "Blue",
Color.Yellow => "Yellow",
_ => "Invalid Color" // 3.
};
-
variable comes before the switch keyword
-
The
case
and:
elements are replaced with =>. -
The default case is replaced with a _ discard.
-
The bodies are expressions (e.g.,
"Red"
), not statements (e.g.,return "Red"
).
Side Note 1: _
Discards
-
A discard is a write-only variable.
-
You can assign all of the values that you intend to discard to the single variable.
-
Discards can be used in:
-
Tuple and object deconstruction.
-
Calls to methods with
out
parameters. -
Pattern matching with
is
andswitch
. -
A standalone
_
when no_
is in scope.
-
Side Note 2: out parameter modifier
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
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
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
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);
}
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
// 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";
}
}
// 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
// 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
// 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)
// 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)
// use out parameter
public string UserService
{
get
{
if (_serviceConf.TryGetValue("User", out object obj))
return obj?.ToString();
return null;
}
}
Exercise: rewrite the code (3)
// use ternary operator
public string UserService
{
get
{
return _serviceConf.TryGetValue("User", out object obj) ? obj?.ToString() : null;
}
}
Exercise: rewrite the code (4)
// use expression-bodied function
public string UserService
{
get => _serviceConf.TryGetValue("User", out object obj) ? obj?.ToString() : null;
}
Exercise: rewrite the code (5)
// Before
public string UserService
{
get
{
object obj = null;
if (_serviceConf.TryGetValue("User", out obj))
return obj?.ToString();
return null;
}
}
// 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.
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";
}
}
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
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
-
C# tuples are types that you define using a lightweight syntax.
-
Basic version introduced before C# 7.0, improved a lot in C# 7.0 release
-
One of the most common uses for tuples is as a method return value.
Example
// 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.
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
};
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
- Simple type patterns
- Relational patterns
- 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.
// 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.
// 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 &&
, ||
, !
.
// 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
-
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)
-
-
Other important concepts
-
Discards
-
Deconstruct
-
out parameter
-
Expression-bodied function
-
Tuples
-
Thanks for listening / reading :)