Overview of scenarios where you can use pattern matching. These techniques may improve the readability and correctness of your code.
Switch statement vs switch expression
First of all we need to clear when should we use which one
Use switch statement (old) when:
- you need to call
void
methods - you need to execute multiple tasks
- the result is not a value, but they’re actions
Use switch expression (new) when:
- you need to map one entry value to one exit value
- you have a single call per case
switch statement
We have the following record
public record Order(string Status, decimal Cost);
and the following void calls (they’re abstract just for the sake of the example as what they do is not important)
public abstract void UpdatePendingOrder(Order order);
public abstract void UpdateCancelledOrder(Order order);
public abstract void UpdateCompletedOrder(Order order);
Example on how to execute a call with secondary task
// DON'T DO THIS
public void NestedIfElseStatement(Order order)
{
if (order.Status == "Pending")
{
UpdatePendingOrder(order);
SendKpis(order);
}
else if (order.Status == "Completed")
{
UpdateCompletedOrder(order);
SendKpis(order);
}
else if (order.Status == "Cancelled")
{
UpdateCancelledOrder(order);
SendEmail(order);
}
else
{
throw new InvalidOperationException("Unknown status");
}
}
As this processes each status executing multiple tasks we’d solve this with a switch statement
public void SwitchStatement(Order order)
{
switch (order.Status)
{
case "Pending":
UpdatePendingOrder(order);
SendKpis(order);
break;
case "Completed":
UpdateCompletedOrder(order);
SendKpis(order);
break;
case "Cancelled":
UpdateCancelledOrder(order);
SendEmail(order);
break;
default:
throw new InvalidOperationException("Unknown status");
}
}
switch expression
We have the following record
public record Reference(int Id, bool Completed);
With the following methods
public abstract Reference GetPendingOrderRef(Order order);
public abstract Reference GetCancelledOrderRef(Order order);
public abstract Reference GetCompletedOrderRef(Order order);
Here we have a process where each status maps to exactly one execution and we have no secondary side effects.
// DON'T DO THIS
public Reference NestedIfElseExpression(Order order)
{
if (order.Status == "Pending")
{
return GetPendingOrderRef(order);
}
else if (order.Status == "Completed")
{
return GetCompletedOrderRef(order);
}
else if (order.Status == "Cancelled")
{
return GetCancelledOrderRef(order);
}
else
{
throw new InvalidOperationException("Unknown status");
}
}
this is the kind of case we may solve through switch expressions
public Reference SwitchExpression(Order order) =>
order.Status switch
{
"Pending" => GetPendingOrderRef(order),
"Completed" => GetCompletedOrderRef(order),
"Cancelled" => GetCancelledOrderRef(order),
_ => throw new InvalidOperationException("Unknown status"),
};
_
is the discard pattern that matches all values. It handles any error conditions where the value doesn’t match one of the defined values.
check and map integer values
public string CheckReferenceId(Reference reference) =>
reference.Id switch
{
<=100 => "system values",
>100 and <=1000 => "test references",
>1000 => "customer order",
};
check and map multiple values at once
public string CheckMultipleValues(Reference reference) =>
reference switch
{
{ Id: <= 100, Completed: false } => "system values - to do",
{ Id: <= 100, Completed: true } => "system values - done",
{ Id: >= 1000, Completed: false } => "customer orders - to ship",
null => throw new ArgumentNullException(nameof(reference), "can't act on null"),
};
check subclasses with multiple values
Now we have the following Order
subclasses
public record Order(string Status, decimal Cost);
public record NAOrder(string Status, decimal Cost) : Order(Status, Cost);
public record EUOrder(string Status, decimal Cost) : Order(Status, Cost);
where we may check the subtype and their properties
public Reference CheckSpecialOrders(Order order) =>
order switch
{
NAOrder o when o.Status == "Cancelled" && o.Cost > 10000 => GetCancelledOrderRef(order),
EUOrder o when o.Status == "Cancelled" && o.Cost > 5000 => GetCancelledOrderRef(order),
_ => throw new InvalidOperationException("Unknown status"),
};
Apply pattern matching to if
With an if
we may check doing this, instead of using multiple if
, and
, or
public void CheckCancelledOrders(Order order)
{
if (order is EUOrder { Status: "Cancelled", Cost: > 10000 })
{
// check and do GDPR compliant things
SendEmail(order);
}
else if (order is NAOrder { Status: "Cancelled", Cost: > 10000 })
{
SendEmail(order);
}
}
null checks
We may use pattern matching to check for null
(instead of !=
)
if (order is null)
throw new InvalidOperationException("cannot operate on null");
list patterns
Since c# 11 we may check for list emptiness
if (orders is [])
throw new ArgumentException("List of orders cannot be empty");
if (orders is not [])
Console.WriteLine("list of orders is not empty");
Reference(s)
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
https://ianvink.wordpress.com/2021/12/11/part-1-c-10-pattern-matching-or-the-death-of-if-else/
https://dev.to/ahmedshahjr/improve-your-c-code-with-pattern-matching-5g7c
https://endjin.com/blog/2023/03/dotnet-csharp-11-pattern-matching-lists