visit
Pattern matching is achieved by using the
switch
operator. Wait a minute... doesn't switch
already exist in the language? Yes, which makes the adoption of this new feature very confusing.Even if the keyword, syntax, and behavior of the
switch
operator are similar, the new feature differs remarkably. One of the biggest differences is that the new switch
is an expression and the old one is a statement. You don't have case and break statements anymore.Expression: Something which returns a value when evaluated
Statement: A line of code executing something
e.g.
Statement
if(a == null)
{
someCode...
}
else
{
someCode2...
}
Expression
a == null ? value1 : value2;
That being said, let's explore the similarities and differences between the 2
switch
operators.Old switch
Syntax
switch(value)
{
case 1: something...
break;
case 2: something2...
case 3: something3...
break;
default:
somethingDefault....
break;
}
The old switch tries to find the next instruction to branch to. Then it executes every line sequentially until it hits a break statement. It behaves like a
goto
statement, which is generally considered a bad practice. This operator only runs code and doesn't return anything, that's why it's considered a statement.New switch
Syntax
var a = value1 switch
{
matchingExpression => expression
matchingExpression2 => expression
...
}
Let's explore a
TollService
application that has many business rules. We'll see how pattern matching makes the code clean and easy to read.V1 - Matching on types
public decimal CalculateToll(object vehicle)
{
// Price per vehicle type
// ==========
// Car -> 2$
// Taxi -> 3.50$
// Bus -> 5$
// Truck -> 10$
return vehicle switch
{
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException("Not a known vehicle type", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
V2 - Matching on properties
You can use the
{}
to match on specific values of your objectpublic decimal CalculateToll(object vehicle)
{
// Price considering occupancy
// ===========
// Car and taxi
// No passengers -> +0.50 $
// 2 passengers -> -0.50 $
// 3 or more passengers -> -1$
return vehicle switch
{
Car { Passengers: 0 } => 2.00m + 0.50m,
Car { Passengers: 1 } => 2.0m,
Car { Passengers: 2 } => 2.0m - 0.50m,
Car _ => 2.00m - 1.0m,
Taxi { Fares: 0 } => 3.50m + 1.00m,
Taxi { Fares: 1 } => 3.50m,
Taxi { Fares: 2 } => 3.50m - 0.50m,
Taxi _ => 3.50m - 1.00m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException("Not a known vehicle type", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
V3 - Conditional boolean expression
You can use the
when
keyword to define a boolean expression that will be evaluated on your object. You may already be familiar with the when
keyword. It was previously introduced in C# 6.0 in catch
statements to evaluate conditions on properties of the exception. As you can imagine, it works exactly the same way here, so you shouldn't be lost.public decimal CalculateToll(object vehicle)
{
// Price considering occupancy
// ===========
// Bus
// Less than 50% full -> +2$
// More than 90% full -> -1$
return vehicle switch
{
// car and taxi hidden here to make it easier to read
// ...
Bus b when (double)b.Riders / (double)b.Capacity < 0.50 => 5.00m + 2.00m,
Bus b when (double)b.Riders / (double)b.Capacity > 0.90 => 5.00m - 1.00m,
Bus _ => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException("Not a known vehicle type", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
V4 - Nesting
switch
operators must return an expression and since they are themselves expressions, it means we can nest them to make our code easier to read.public decimal CalculateToll(object vehicle)
{
return vehicle switch
{
Car c => c.Passengers switch
{
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
Taxi t => t.Fares switch
{
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},
// Bus and truck hidden here to make it easier to read
// ...
};
}
Special match expressions
You may have noticed in the previous examples that we've been using some special matching expressions,
{}
, null
and _
.{}
: Non-null, but an unknown typenull
: null value_
: Wildcard that matches absolutely anything but doesn't capture the value in a new variableGotcha
Matching expressions are evaluated from top to bottom. Always put the most restrictive matching expression first and try to finish with the 'special' match expressions.
e.g. If you use
_
as the first line, it will always match on this expression and nothing else will be evaluated.All code samples are available on
Previously published at //blog.miguelbernard.com/pattern-matching-in-csharp/