Last active
June 17, 2021 12:47
-
-
Save rstropek/307503ff0a76d21ed8e5a3089c11392f to your computer and use it in GitHub Desktop.
C# Pattern Matching Inside Out
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using static System.Console; | |
// We can simply replace == with is. Doesn't make much sense, though. | |
int someNumber = 42; | |
if (someNumber is 42) WriteLine("Some number is 42"); | |
// Situation changes when we change the type to object. | |
object something = 42; | |
// The following statement would not work (you cannot use == with | |
// object and int). We need to use `Equals` instead. | |
// if (something == 42) ... | |
if (something.Equals(42)) WriteLine("Something is 42"); | |
// Now we can write this much nicer with Constant Patterns: | |
if (something is 42) WriteLine("Something is 42"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using static System.Console; | |
// The var pattern doesn't make much sense when used on its own. | |
int someNumber = 42; | |
if (someNumber is var x) WriteLine(x); | |
// It enables new possibilities in combination with other | |
// language features, in particular with switch. | |
switch (someNumber) | |
{ | |
case 41: WriteLine("fourtyone"); break; | |
case 43: WriteLine("fourtythree"); break; | |
case var n when n % 2 == 0: WriteLine("even"); break; | |
default: throw new NotImplementedException(""); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using static System.Console; | |
// This is how we would have done type checking in the past without patterns. | |
static void GoodOldTypeCheck() | |
{ | |
object o = new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false); | |
var h = o as Hero; | |
if (h != null) WriteLine($"o is a Hero and is called {h.HeroName}"); | |
} | |
GoodOldTypeCheck(); | |
// Now we can be much more concise using a "Type Pattern": | |
static void NewTypePattern() | |
{ | |
object o = new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false); | |
// +--- Type Pattern | |
if (o is Hero h) | |
{ | |
// h is now available and has the correct type Hero. | |
WriteLine($"h is of type {h.GetType().Name}"); | |
WriteLine($"o is a Hero and is called {h.HeroName}"); | |
} | |
// var h = 42; // The variable name h is now taken | |
//-> this statement would not work | |
// WriteLine(h.HeroName); | |
// does not work because h is unassigned | |
// outside of `if` block | |
} | |
NewTypePattern(); | |
// Also works nice with collections | |
static void TypePatternAndCollections() | |
{ | |
IEnumerable<Person> pEnumerable = new Person[] | |
{ | |
new("John", "Doe"), | |
new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false) | |
}; | |
// +-- Two Type Patterns, 2nd depends on 1st ----+ | |
if (pEnumerable is IReadOnlyList<Person> pList && pList[1] is Hero h) | |
{ | |
WriteLine($"o is a Hero and is called {h.HeroName}"); | |
} | |
} | |
TypePatternAndCollections(); | |
static void NullCheckWithTypePattern() | |
{ | |
Hero? someone = null; | |
// +-- Type Pattern +-- h can be used in subsequent parts of expression | |
if (someone is Hero h && h.Type == HeroType.FailedExperiment) | |
{ | |
WriteLine($"Someone is the {h.HeroName} hero and not null"); | |
} | |
else WriteLine("Someone is null"); | |
// +-- Constant pattern with null | |
if (someone is null) WriteLine("Someone is null"); | |
} | |
NullCheckWithTypePattern(); | |
enum HeroType { NuclearAccident, FailedExperiment, Alien, Mutant, Technology, Other }; | |
enum HeroTypeCategory { Accident, SuperPowersFromBirth, Other } | |
enum VoughtEmployeeType { TopManagement, TheSeven, LocalHero, RegularPerson }; | |
record Person(string FirstName, string LastName, int? Age = null, Person? Assistant = null); | |
record Hero(string FirstName, string LastName, string HeroName, HeroType Type, | |
bool CanFly, Person? Assistant = null) : Person(FirstName, LastName, Assistant: Assistant); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using static System.Console; | |
static void TypePatternInSwitch() | |
{ | |
object o = new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false); | |
switch (o) | |
{ | |
// +--- Constant Pattern | |
case "FooBar": | |
WriteLine("It is a string and it is FooBar"); | |
break; | |
// +--- Type Pattern | |
case string s: | |
WriteLine($"o is a string and contains {s}"); | |
break; | |
// +--- Type Pattern | |
case Hero h: | |
WriteLine($"o is a Hero and is called {h.HeroName}"); | |
break; | |
// +--- Type Pattern | |
case Person p: | |
WriteLine($"o is a Person and has name {p.FirstName} {p.LastName}"); | |
break; | |
// Try moving this case above the `Hero` case -> will result in an error | |
// because the `Hero` case is no longer reachable (every `Hero` is also a `Person`). | |
// +--- Var Pattern, catches everything | |
case var obj: | |
WriteLine($"o is just an object of type {obj.GetType().Name}"); | |
break; | |
default: | |
throw new InvalidOperationException("Hmm, this should never happen..."); | |
} | |
} | |
TypePatternInSwitch(); | |
static void SwitchWithWhen() | |
{ | |
object o = new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false); | |
switch (o) | |
{ | |
// +--- Type Pattern | |
// | +--- Using resulting variable in when expression. | |
case Hero h when h.Type == HeroType.FailedExperiment: | |
WriteLine($"o Hero {h.HeroName} and became it because of an failed experiment"); | |
break; | |
// +--- Type Pattern | |
case Hero h: | |
WriteLine($"o is Hero {h.HeroName}, NOT because of a failed experiment"); | |
break; | |
default: | |
throw new InvalidOperationException("Hmm, this should never happen..."); | |
} | |
} | |
SwitchWithWhen(); | |
enum HeroType { NuclearAccident, FailedExperiment, Alien, Mutant, Technology, Other }; | |
enum HeroTypeCategory { Accident, SuperPowersFromBirth, Other } | |
enum VoughtEmployeeType { TopManagement, TheSeven, LocalHero, RegularPerson }; | |
record Person(string FirstName, string LastName, int? Age = null, Person? Assistant = null); | |
record Hero(string FirstName, string LastName, string HeroName, HeroType Type, | |
bool CanFly, Person? Assistant = null) : Person(FirstName, LastName, Assistant: Assistant); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using static System.Console; | |
static void BasicRecursivePattern() | |
{ | |
Person someone = new Hero("Tony", "Stark", "Iron Man", HeroType.Technology, true); | |
// Good old style: | |
// +-- Pattern matching | |
if (someone is Hero hero && hero.CanFly) WriteLine($"We have a flying hero {hero.HeroName}"); | |
// Recursive Pattern | |
// +-- Type Pattern | |
// | +-- Constant Pattern | |
// | | +-- Var Pattern | |
if (someone is Hero { CanFly: true, HeroName: var name }) | |
{ | |
WriteLine($"We have a flying hero {name}"); | |
} | |
object somebody = new Person("Foo", "Bar", 42); | |
// +-- Type Pattern | |
// | +-- Relational Pattern | |
if (somebody is Person { Age: > 40 } p) WriteLine("We have an old person."); | |
// +-- Empty Recursive Pattern (doesn't make sense here) | |
if (somebody is { }) WriteLine("Somebody is something"); | |
// Makes sense in `switch` expression | |
WriteLine(somebody switch | |
{ | |
Hero h => $"Hero {h.HeroName}", | |
Person pers => $"Person {pers.FirstName}", | |
{ } => "Sombody not null", | |
null => throw new InvalidOperationException() | |
}); | |
} | |
BasicRecursivePattern(); | |
static void DoubleRecursivePattern() | |
{ | |
object bm = new Hero("Bruce", "Wayne", "Batman", HeroType.Technology, false, | |
new Hero("Robin", string.Empty, "Robin", HeroType.Technology, false)); | |
// +-- Type Pattern | |
// | +-- Type Pattern | |
// | | +-- Var Pattern | |
if (bm is Hero { Assistant: Hero { HeroName: var aName } }) | |
{ | |
WriteLine($"We have a hero who has a hero assistant named {aName}"); | |
} | |
} | |
DoubleRecursivePattern(); | |
static void AssignmentAndBoolExpression() | |
{ | |
Person someone = new Hero("Tony", "Stark", "Iron Man", HeroType.Technology, true); | |
// You can use recursive patterns in bool assignments, too. | |
// Recursive Pattern ---+ | |
var isHeroBecauseOfTech = someone is Hero { Type: HeroType.Technology }; | |
if (isHeroBecauseOfTech) WriteLine("Hero because of tech"); | |
if (someone is Hero { Type: HeroType.Technology }) WriteLine("Hero because of tech"); | |
} | |
AssignmentAndBoolExpression(); | |
enum HeroType { NuclearAccident, FailedExperiment, Alien, Mutant, Technology, Other }; | |
enum HeroTypeCategory { Accident, SuperPowersFromBirth, Other } | |
enum VoughtEmployeeType { TopManagement, TheSeven, LocalHero, RegularPerson }; | |
record Person(string FirstName, string LastName, int? Age = null, Person? Assistant = null); | |
record Hero(string FirstName, string LastName, string HeroName, HeroType Type, | |
bool CanFly, Person? Assistant = null) : Person(FirstName, LastName, Assistant: Assistant); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using static System.Console; | |
using System.Linq; | |
static void SwitchExpression() | |
{ | |
object o = new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false); | |
// Note that here comes a switch EXPRESSION, NOT a switch STATEMENT. | |
WriteLine(o switch | |
{ | |
Hero { HeroName: var n, CanFly: true } => $"Hero {n} that can fly", | |
Hero h => $"Hero {h.HeroName} {h.CanFly}", | |
Person p => $"Person {p.LastName}", | |
_ => "Who is that?!?" | |
}); | |
// Switch expression with when | |
WriteLine(o switch | |
{ | |
Hero h2 when h2.CanFly => 2, | |
Hero => 1, | |
Person => 0, | |
_ => throw new InvalidOperationException() | |
}); | |
} | |
SwitchExpression(); | |
static void SwitchWithTuples() | |
{ | |
var people = new[] | |
{ | |
(Name: "Stan Edgar", Type: VoughtEmployeeType.TopManagement, CanFly: false, Popularity: 0), | |
(Name: "Homelander", Type: VoughtEmployeeType.TheSeven, CanFly: true, Popularity: 10), | |
(Name: "The Deep", Type: VoughtEmployeeType.LocalHero, CanFly: false, Popularity: 2) | |
}; | |
// In the following switch expression, note the C# discard operator. | |
// Note the switch expression in combination with LINQ. | |
var yearlySalary = people.Select(p => p switch | |
{ | |
("Stan Edgar", VoughtEmployeeType.TopManagement, _, _) => 900_000_000, | |
(_, VoughtEmployeeType.TopManagement, _, _) => 10_000_000, | |
("Homelander", VoughtEmployeeType.TheSeven, true, >= 9) => 100_000_000, | |
("Homelander", VoughtEmployeeType.TheSeven, true, _) => 50_000_000, | |
(_, VoughtEmployeeType.TheSeven, true, _) => 25_000_000, | |
(_, VoughtEmployeeType.TheSeven, _, _) => 10_000_000, | |
(_, VoughtEmployeeType.LocalHero, _, _) => 1_000_000, | |
_ => 25_000 | |
}); | |
WriteLine(yearlySalary.Sum()); | |
var peopleRecords = new Hero[] | |
{ | |
new("Carl", "Lucas", "Luka Cage", HeroType.FailedExperiment, false), | |
new("Danny", "Rand", "Iron Fist", HeroType.Other, false) | |
}; | |
var yearlySalaryFromRecords = peopleRecords.Select(p => p switch | |
{ | |
{ CanFly: true, Type: _ } => 100_000_000, // You could omit the discard operator here | |
{ CanFly: _, Type: HeroType.FailedExperiment } => 50_000_000, | |
_ => 1_000_000 | |
}); | |
WriteLine(yearlySalaryFromRecords.Sum()); | |
} | |
SwitchWithTuples(); | |
static void RecursivePatternInSwitch() | |
{ | |
var h = new Hero("Wade", "Wilson", "Deadpool", HeroType.FailedExperiment, false); | |
var ht = h switch | |
{ | |
{ Type: HeroType.NuclearAccident } => HeroTypeCategory.Accident, | |
{ Type: HeroType.FailedExperiment } => HeroTypeCategory.Accident, | |
{ Type: HeroType.Alien } => HeroTypeCategory.SuperPowersFromBirth, | |
{ Type: HeroType.Mutant } => HeroTypeCategory.SuperPowersFromBirth, | |
_ => HeroTypeCategory.Other | |
}; | |
WriteLine($"Hero of type {ht}"); | |
} | |
RecursivePatternInSwitch(); | |
enum HeroType { NuclearAccident, FailedExperiment, Alien, Mutant, Technology, Other }; | |
enum HeroTypeCategory { Accident, SuperPowersFromBirth, Other } | |
enum VoughtEmployeeType { TopManagement, TheSeven, LocalHero, RegularPerson }; | |
record Person(string FirstName, string LastName, int? Age = null, Person? Assistant = null); | |
record Hero(string FirstName, string LastName, string HeroName, HeroType Type, | |
bool CanFly, Person? Assistant = null) : Person(FirstName, LastName, Assistant: Assistant); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using static System.Console; | |
static void SimpleRelational() | |
{ | |
var age = 42; | |
// +-- Relational Pattern | |
if (age is >= 42) WriteLine("High"); | |
int? unknownAge = null; | |
// +-- Negation | |
if (unknownAge is not >= 42) WriteLine("Low or null"); | |
} | |
SimpleRelational(); | |
static void RelationalCombination() | |
{ | |
var letter = 'x'; | |
// +- Combination and/or -+ | |
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'); | |
WriteLine(IsLetter(letter)); | |
var number = 42; | |
// +-- Combinator | |
static bool IsInRange(int n) => n is > 40 and < 50; | |
WriteLine(IsInRange(number)); | |
object someObject = 420; | |
// +-- Type Pattern... | |
// | +-- ...combined with relational pattern | |
if (someObject is int and not < 42) WriteLine("High Number"); | |
} | |
RelationalCombination(); | |
static void RelationalInSwitch() | |
{ | |
var p = new Person("Foo", "Bar", 13); | |
var ag = p.Age switch // <--------------+ | |
{// | | |
// +-- Relational Pattern in switch --+ | |
// V | |
< 13 => "child", | |
< 18 => "teenager", | |
< 65 => "adult", | |
_ => "senior" | |
}; | |
WriteLine($"Person is in age group {ag}"); | |
} | |
RelationalInSwitch(); | |
static void PatternsAndTuples() | |
{ | |
var h = (HeroName: "Stormfront", CanFly: true); | |
// +-- Discard operator | |
// | +-- Constant Pattern | |
if (h is (_, true)) WriteLine("The hero can fly"); | |
var p = (Name: "Foo Bar", Age: 42); | |
// +-- Constant Pattern | |
// | +-- Relational Pattern | |
if (p is (var n, > 40)) WriteLine($"Person with name {n} is not young anymore"); | |
} | |
PatternsAndTuples(); | |
enum HeroType { NuclearAccident, FailedExperiment, Alien, Mutant, Technology, Other }; | |
enum HeroTypeCategory { Accident, SuperPowersFromBirth, Other } | |
enum VoughtEmployeeType { TopManagement, TheSeven, LocalHero, RegularPerson }; | |
record Person(string FirstName, string LastName, int? Age = null, Person? Assistant = null); | |
record Hero(string FirstName, string LastName, string HeroName, HeroType Type, | |
bool CanFly, Person? Assistant = null) : Person(FirstName, LastName, Assistant: Assistant); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment