Skip to content

Instantly share code, notes, and snippets.

@rstropek
Last active June 17, 2021 12:47
Show Gist options
  • Save rstropek/307503ff0a76d21ed8e5a3089c11392f to your computer and use it in GitHub Desktop.
Save rstropek/307503ff0a76d21ed8e5a3089c11392f to your computer and use it in GitHub Desktop.
C# Pattern Matching Inside Out
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");
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("");
}
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);
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);
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);
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);
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