Using Relational Patterns for Range Checks in C
Pattern matching in modern C# significantly improves code clarity,
and the introduction of relational patterns provides an elegant way to
evaluate numerical boundaries. This article explores how relational
patterns streamline range validation inside switch
expressions, comparing traditional verbose approaches with the modern
syntax that combines relational and logical patterns for cleaner, more
maintainable code.
The Problem with Traditional Range Checking
Before relational patterns were introduced in C# 9, evaluating ranges
inside switch constructs required verbose code. Developers typically
relied on extensive if-else chains or switch expressions
packed with repetitive when guard clauses:
// Traditional approach using 'when' clauses
string GetTemperatureDescription(int temp) => temp switch
{
int t when t < 0 => "Freezing",
int t when t >= 0 && t <= 15 => "Cold",
int t when t > 15 && t <= 25 => "Comfortable",
_ => "Hot"
};This pattern required declaring a discarded variable (t)
in every arm and repeating the comparison target, obscuring the actual
business logic behind syntactic noise.
Simplifying with Relational Patterns
Relational patterns allow you to use standard comparison
operators—<, <=, >, and
>=—directly against the input value without declaring a
temporary variable or using the when keyword.
Here is the previous example rewritten using relational patterns:
// Modern approach using relational and logical patterns
string GetTemperatureDescription(int temp) => temp switch
{
< 0 => "Freezing",
>= 0 and <= 15 => "Cold",
> 15 and <= 25 => "Comfortable",
_ => "Hot"
};Combining Relational and Logical Patterns
Relational patterns become especially effective when combined with
the logical pattern combinators and, or, and
not. Instead of relying on C#'s traditional binary
operators (&&, ||, !),
switch expressions use readable keywords to create expressive
boundaries:
string GetGrade(double score) => score switch
{
< 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
>= 90 => "A",
>= 80 and < 90 => "B",
>= 70 and < 80 => "C",
>= 60 and < 70 => "D",
_ => "F"
};Key Advantages
- Elimination of Boilerplate: You avoid naming temporary variables or writing redundant references to the tested expression.
- Improved Readability: Range checks read like mathematical interval notations, making business rules immediately clear at a glance.
- Compiler Diagnostics: The C# compiler can evaluate pattern combinations more effectively, warning you about unreachable switch arms or overlapping patterns that will never execute.