C# pattern matching allows you to check whether a value satisfies a condition and work with that value when the condition matches. It simplifies decision-making by combining type checks, comparisons, null checks, and property validation into readable expressions. Instead of writing multiple conditional statements and manually casting objects, you can use patterns to express complex logic with less code.
Pattern matching is commonly used when validating user input, processing objects of different types, checking numerical ranges, and implementing business rules. C# supports several pattern types, including constant, type, relational, logical, property, positional, and list patterns. These patterns can be used with if statements, switch statements, and switch expressions.
Type patterns in C#
A type pattern checks whether an object is compatible with a specified type. You can also declare a variable to access the value after the type check succeeds. This eliminates the need for a separate cast and makes the code easier to maintain.
static string DescribeValue(object? value)
{
if (value is string text)
{
return $"Text contains {text.Length} characters.";
}
if (value is int number)
{
return $"Integer value: {number}";
}
return "Unsupported value.";
}
In value is string text, C# checks the runtime type and makes the matched value available through the text variable. Similarly, value is int number identifies an integer and exposes it through number. This approach is particularly useful when working with objects whose runtime types may differ.
Relational and logical patterns
Relational patterns compare values using operators such as <, >, <=, and >=. Logical patterns combine conditions using and, or, and not. Together, they make it easier to represent ranges and validation rules without creating long chains of conditional statements.
static string GetGrade(int score) => score switch
{
< 0 or > 100 => "Invalid score",
>= 90 => "Excellent",
>= 75 and < 90 => "Very good",
>= 60 and < 75 => "Passing",
_ => "Needs improvement"
};
The first pattern rejects scores outside the valid range. The remaining patterns classify the score into a category, while the discard pattern _ handles values that do not match earlier arms. In a switch expression, every arm must produce a compatible result. C# also checks for unreachable patterns, helping prevent invalid branching logic.
Property patterns for objects
Property patterns inspect an object's properties directly. They are useful when a decision depends on multiple properties, such as an employee's experience and account status, without requiring separate property comparisons.
public record Employee(
string Name,
int Experience,
bool IsActive);
static string GetEmployeeStatus(Employee? employee) =>
employee switch
{
null => "Employee not found",
{ IsActive: false } => "Account is inactive",
{ Experience: >= 5, IsActive: true } =>
"Experienced active employee",
{ IsActive: true } => "Active employee",
_ => "Unclassified employee"
};
The pattern { Experience: >= 5, IsActive: true } checks two properties in a single expression. The null pattern handles missing employee data before any property access is attempted. More specific patterns should appear before broader patterns when their conditions overlap, ensuring that the intended branch can be reached.
Switch expressions versus switch statements
A switch expression evaluates an input and returns a result. It is useful when different conditions map directly to values, such as converting status codes into messages or assigning categories to scores. A switch statement is preferable when each branch needs to perform several operations, such as logging, updating application state, or calling multiple methods.
static string GetStatusMessage(int statusCode)
{
switch (statusCode)
{
case 200:
return "Request completed";
case 404:
return "Resource not found";
case 500:
return "Server error";
default:
return "Unknown status";
}
}
Both switch constructs support pattern matching. Choose the expression form when the primary purpose is to calculate a value, and use the statement form when explicit control flow and multiple actions make the code clearer.
Null checks and safe pattern matching
Pattern matching provides a clear way to handle nullable values. The expression value is null checks whether a value is null, while value is not null verifies that it is present. Nullable reference type analysis can recognize successful null checks and help prevent accidental null dereferences.
static string FormatName(string? name)
{
return name is { Length: > 0 }
? name.Trim()
: "Unknown";
}
The property pattern matches a non-null string whose length is greater than zero. However, a string containing only whitespace also satisfies this condition. If the application requires meaningful text, use string.IsNullOrWhiteSpace(name) to handle both null and whitespace-only input.
Best practices for pattern matching
- Place specific patterns before general patterns when conditions overlap.
- Use switch expressions when each branch naturally produces a value.
- Prefer property patterns when several object properties determine the result.
- Keep additional
whenconditions simple and easy to understand. - Handle null values and unexpected inputs according to the application's requirements.
- Avoid complicated nested patterns when a straightforward conditional statement is easier to read.
- Check your project's C# language version before using newer pattern features.
Different pattern-matching features were introduced in different C# versions. Relational and logical patterns became available in C# 9, while list patterns were introduced in C# 11. Understanding these version differences helps you choose syntax compatible with your project. To explore the fundamentals of branching logic, continue with conditional statements in C#.