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 isint 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 patternpublicstaticstring BasicSwitch(paramsstring[] parts) {
switch (parts.Length)
{
case0:
return"No elements to the input";
case1:
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 patternpublicstaticstring BasicSwitch(paramsstring[] parts) {
switch (parts.Length)
{
case0:
return"No elements to the input";
case1:
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 patternpublicstaticdouble 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
publicstaticdouble ComputeAreaCaseWhen(object shape)
{
switch (shape)
{
case Square s when s.Side == 0:
return0;
case Triangle t when t.Base == 0 || t.Height == 0:
return0;
case Square s:
return s.Side * s.Side;
casenull: // a special case for null handlingthrownew ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
default: // won't be nullreturn -1;
}
}
C# 7.0: Cases can be grouped
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicstaticdouble ComputeAreaCaseWhen(object shape)
{
switch (shape)
{
case Square s when s.Side == 0:
case Triangle t when t.Base == 0 || t.Height == 0:
return0;
case Square s:
return s.Side * s.Side;
casenull: // a special case for null handlingthrownew ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
default: // won't be nullreturn -1;
}
}
// C# 7.0staticstring 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";
}
}
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 methodpublicstring GetName() {
return$"{FirstName} {LastName}";
}
// After: implement Expression-bodied function publicstring 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
// methodpublicstring GetName() => $"{FirstName} {LastName}";
// read-only propertypublicreadonlystring FullName => $"{FirstName} {LastName}";
// constructorpublic ExpressionMembersExample(string label) => this.Label = label;
// finalizer~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");
// Expression-bodied get / set accessors.privatestring _fullname;
publicstring 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 simplifiedpublicstring UserService
{
get {
object obj = null;
if (_serviceConf.TryGetValue("User", out obj))
return obj?.ToString();
returnnull;
}
}
Exercise: rewrite the code (2)
1
2
3
4
5
6
7
8
9
10
// use out parameterpublicstring UserService
{
get {
if (_serviceConf.TryGetValue("User", outobject obj))
return obj?.ToString();
returnnull;
}
}
Exercise: rewrite the code (3)
1
2
3
4
5
6
7
8
// use ternary operatorpublicstring UserService
{
get {
return _serviceConf.TryGetValue("User", outobject obj) ? obj?.ToString() : null;
}
}
Exercise: rewrite the code (4)
1
2
3
4
5
// use expression-bodied functionpublicstring UserService
{
get => _serviceConf.TryGetValue("User", outobject obj) ? obj?.ToString() : null;
}
Exercise: rewrite the code (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
// Beforepublicstring UserService
{
get {
object obj = null;
if (_serviceConf.TryGetValue("User", out obj))
return obj?.ToString();
returnnull;
}
}
staticstring 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
staticstring DisplayWithPropertyPatterns1(object o) => o switch{
Point { X: 0, Y: 0 } p => "origin",
Point { X: var x, Y: var y } p => $"({x}, {y})",
_ => "unknown"};
staticstring DisplayWithPropertyPatterns2(object o) => o switch{
Point { X: 0, Y: 0 } => "origin",
Point { X: var x, Y: var y } => $"({x}, {y})",
{} => o.ToString(), // {} = "not-null" patternnull => "null"};
Technique: Switch within switch
1
2
3
4
5
6
7
8
9
10
11
12
staticstring 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.
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.0staticstring DisplayWithPropertyPatterns1(object o) => o switch {
Point { X: 0, Y: 0 } p => "origin",
Point _ => "Not an original point",
_ => "Not a point" };
// C# 9.0staticstring 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.0publicstaticdecimal 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,
_ => thrownew ArgumentException("Not a known vehicle type", nameof(vehicle))
};
// C# 9.0publicstaticdecimal 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,
},
_ => thrownew 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.0publicstaticdecimal 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 => thrownew ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => thrownew ArgumentNullException(nameof(vehicle))
};
// Use not in if-conditionsif (!(e is Customer)) { ... }
if (e is not Customer) { ... }