C# Null Pattern Matching: Syntax and Practical Usage
c# null pattern matching: Learn how to use C# null pattern matching to simplify null checks, combine with property patterns, and write cleaner conditional logic.
When working with reference types or nullable value types in C#, checking for null is a recurring task. The traditional == null comparison works, but C# pattern matching gives you a more expressive and often more readable way to express the same intent. The is null and is not null patterns are the core of c# null pattern matching, and they integrate naturally with other pattern matching features like property patterns and switch expressions.
The Basics: is null and is not null
The simplest form of null pattern matching uses the is operator with the null constant pattern. Instead of writing if (value == null), you can write if (value is null). The two are equivalent for most reference types, but the pattern syntax reads more clearly and can be combined with other patterns.
string? name = GetName(); if (name is null) { Console.WriteLine("Name is null"); } else { Console.WriteLine($"Name is {name}"); }
The is not null pattern is the inverse. It is especially useful when you want to execute a block only when a value is present, without needing an explicit else.
if (name is not null) { Console.WriteLine($"Hello, {name}"); }
This syntax is not just cosmetic. It makes the null check explicit and avoids the risk of accidentally invoking an overloaded == operator that might behave differently for custom types. The pattern matching approach always performs a direct reference or nullable value check, regardless of any overloaded equality operators.
Using Null Patterns in Conditional Expressions
The is null and is not null patterns work in any boolean context, including the conditional operator ?:. This can make inline null handling more concise.
string displayName = name is null ? "Unknown" : name;
While this is equivalent to name ?? "Unknown", the pattern version is useful when you need to combine the null check with other conditions. For example, you might want to treat an empty string as if it were null:
if (name is null || name.Length == 0) { Console.WriteLine("Name is empty or null"); }
Pattern matching also works with nullable value types. For an int?, the is null pattern checks whether the HasValue property is false, and is not null checks whether it has a value.
int? count = GetCount(); if (count is not null) { int actualCount = count.Value; Console.WriteLine($"Count is {actualCount}"); }
This is more readable than checking HasValue directly, and it integrates with other patterns that operate on the underlying value.
Combining Null Checks with Property Patterns
One of the most useful aspects of c# null pattern matching is the ability to combine a null check with a property pattern. The { and } syntax lets you inspect properties of an object only when the object is not null. The compiler automatically inserts the null check for you.
Consider a Customer class with an Address property that might be null. Instead of writing a nested if statement, you can use a property pattern:
if (customer is { Address: { City: "Seattle" } }) { Console.WriteLine("Customer lives in Seattle"); n}
This single condition checks that customer is not null, that customer.Address is not null, and that customer.Address.City equals "Seattle". If any of those intermediate objects is null, the whole pattern fails and the condition evaluates to false. This eliminates the need for explicit null checks at each level.
The same pattern works with the not modifier to invert the entire match:
if (customer is not { Address: { City: "Seattle" } }) { Console.WriteLine("Customer does not live in Seattle"); }
Property patterns can also bind values to variables using the var pattern. This is useful when you need to access a nested property after confirming it exists:
if (customer is { Address: { City: var city } }) { Console.WriteLine($"City: {city}"); }
The variable city is of type string and is guaranteed to be non-null because the pattern only matches when the property exists and is not null.
Null Pattern Matching in Switch Expressions
Switch expressions in C# 8 and later support pattern matching directly. You can use null as a case to handle missing values elegantly. This is especially useful when you have multiple branches based on the shape of an object.
string Describe(object? obj) => obj switch { null => "The object is null", int i => $"Integer: {i}", string s => $"String: {s}", _ => "Unknown type" };
The null case is checked first, so you don't need to worry about the other patterns matching a null reference. The discard pattern _ acts as a fallback for any other value.
Switch expressions also work with property patterns. You can combine a null check with a property condition in a single arm:
string GetCityLabel(Customer? customer) => customer switch { null => "No customer", { Address: { City: "Seattle" } } => "Seattle resident", { Address: { } } => "Has an address", _ => "No address" };
Here, the second arm matches a non-null customer with an address in Seattle. The third arm matches any customer that has a non-null Address property, regardless of the city. The order matters: the more specific patterns come first, and the null pattern ensures that the subsequent property patterns are not evaluated on a null reference.
Recursive Patterns and Nested Null Checks
C# pattern matching is recursive, meaning you can nest patterns arbitrarily deep. This is where the real power of null pattern matching shows up when dealing with complex object graphs. Instead of writing a chain of if (a != null && a.B != null && a.B.C != null), you can express the entire condition as a single pattern.
if (order is { Customer: { Address: { Country: "US" } } }) { // Apply US-specific tax rules }
This pattern checks that order, order.Customer, and order.Customer.Address are all non-null, and that the Country property equals "US". If any link in the chain is null, the pattern fails gracefully.
You can also use the and and or combinators to build more complex conditions. For example, to check for a US or Canadian address:
if (order is { Customer: { Address: { Country: "US" or "Canada" } } }) { // Apply North American shipping rules }
The or pattern inside the property pattern allows multiple acceptable values for the Country property without repeating the nested structure.
Performance and Maintainability Considerations
Pattern matching in C# is compiled to efficient code. For simple is null checks, the generated IL is essentially the same as a direct reference comparison. The compiler does not introduce boxing or additional method calls for value types. For property patterns, the compiler generates short-circuiting code that checks each level in order, so there is no performance penalty compared to a manually written nested if.
From a maintainability perspective, pattern matching reduces the number of lines you need to write and makes the intent explicit. A nested property pattern is easier to read than a chain of && conditions, especially when the null checks are interleaved with value comparisons. It also reduces the risk of forgetting a null check, because the pattern syntax forces you to account for the possibility of null at each level.
One tradeoff is that pattern matching can be less familiar to developers who have not used it before. The syntax is concise, but it requires some learning. However, once you are comfortable with it, the code becomes more declarative and less cluttered with boilerplate null guards.
Another consideration is that pattern matching does not change the nullability analysis performed by the compiler. If you use a pattern to check for null, the compiler still understands that the variable is not null in the subsequent block. For example:
if (customer is not null) { // The compiler knows customer is not null here Console.WriteLine(customer.Name); }
This works with nullable reference types as well. The pattern is not null is recognized by the null-state analysis, so you get the same flow-sensitive typing as you would with != null.
Common Mistakes and How to Avoid Them
One common mistake is using is null on a value type that is not nullable. For example, int x = 5; if (x is null) will not compile because int cannot be null. The compiler gives an error, so this is caught early. But when working with generics, you need to be careful. A generic type T might be a reference type or a value type. The pattern x is null is allowed, but it will always be false for non-nullable value types. If you need to handle both, use EqualityComparer<T>.Default or check default(T) instead.
Another mistake is overusing property patterns in places where a simple null-coalescing operator would be clearer. For example, if (customer?.Address?.City == "Seattle") is often more readable than a nested property pattern, especially when you only need a single value. Property patterns shine when you need to match multiple properties or when you want to bind variables. Choose the tool that best expresses the condition.
Finally, be aware that the is null pattern does not invoke any overloaded == operator. This is usually desirable, but if you have a custom type that defines == to treat certain values as equal to null, the pattern will not respect that. In such cases, you might need to use the == operator explicitly. This is rare, but it is worth knowing when you are designing custom equality semantics.
Pattern matching also works with null in switch statements and expressions, but the order of cases matters. In a switch expression, the null arm must come before any arm that uses property patterns on the same object, because the property pattern would throw a NullReferenceException if the object is null. The compiler does not enforce this ordering, so you must be careful to place the null case first.
When you combine multiple patterns with and or or, remember that the and pattern requires both sides to match, while or requires either. If you write x is null or 0, it matches when x is null or when x equals 0. This is a concise way to handle both null and a default value, but it can be confusing if you are not used to the syntax. Always test these combinations with nullable inputs to ensure the behavior is what you expect.
C# null pattern matching is a powerful tool that becomes more valuable as you work with complex object graphs. By using is null, is not null, property patterns, and switch expressions, you can write null-safe code that is both concise and expressive. The key is to understand the syntax and to use it where it improves clarity, not just for the sake of using a new feature.