Pattern matching and records arrived in the same era and answer the same question from two sides. A pattern says what a value is without a cast and a null check; a record says what a value carries without a hand-written Equals, GetHashCode and ToString.
Test a type with a pattern, not with as
An is pattern combines the type test, the cast and the null check in one expression, and the compiler knows the variable is not null inside the branch. The as form needs two statements and leaks a nullable value.
Preferred
if (shape is Circle circle)
{
Draw(circle.Radius);
}Avoid
var circle = shape as Circle;
if (circle != null)
{
Draw(circle.Radius);
}Corpus: Safely cast using pattern matching, is and as · Type patterns
Compare against null with is null
is null and is not null cannot be intercepted by an overloaded == operator, so they mean what they say on a type that also defines equality. They also read as the null test rather than as an arithmetic comparison.
Preferred
if (value is not null)
{
Use(value);
}Avoid
if (value != null)
{
Use(value);
}A switch expression maps a value, not a flow
When each branch produces a value, a switch expression says so in one place and the compiler checks that every path returns. The statement form is for branches that do work, not for choosing a result.
Preferred
var name = size switch
{
<= 0 => "empty",
< 10 => "small",
_ => "large",
};Avoid
string name;
switch (size)
{
case int s when s <= 0: name = "empty"; break;
default: name = "large"; break;
}Corpus: switch expression · Selection statements
Match the shape with property patterns
A property pattern reads a nested shape without walking into it: no temporary variable, no chain of null checks in front. Combined with and, or and not, it replaces most hand-written conditions about data.
Preferred
if (order is { Customer.Country: "DE", Total: > 100 })Avoid
if (order.Customer != null && order.Customer.Country == "DE" && order.Total > 100)Corpus: Pattern matching
Add a condition with when
A guard clause keeps the pattern about shape and the condition about the value. It also keeps the condition inside the switch, so the reader sees the whole decision in one place.
Preferred
case Shape shape when shape.Area > 100:Avoid
case Shape shape:
if (shape.Area <= 100) break;
// ... the guard is now a flow control decisionCorpus: Pattern matching
Match lists by their shape
A list pattern matches an array or span by length and by the items at its ends, which is the readable form of indexing arithmetic. Empty, single-element and head-and-rest checks all fit in the pattern.
Preferred
if (values is [var first, .., var last])Avoid
if (values.Length > 0 && values[0] == first && values[^1] == last)Corpus: Patterns
Keep a switch exhaustive
A switch expression on a closed set such as an enum should handle every member and let the compiler prove it, with an exception in place of _ if a new member must not be ignored. A silent _ arm turns a new enum value into a wrong answer.
Preferred
var verb = state switch
{
State.Open => "open",
State.Closed => "closed",
_ => throw new ArgumentOutOfRangeException(nameof(state)),
};Avoid
var verb = state switch { State.Open => "open", _ => "closed" };Corpus: Selection statements
Use a record for a value that travelsCA1067
A record gives value equality, GetHashCode, ToString and a non-destructive with for free, and the generated Equals follows the fields. A class with the same members hand-written drifts the moment a field is added.
Preferred
public sealed record Money(decimal Amount, string Currency);Avoid
public class Money { /* Amount, Currency, Equals, GetHashCode, ToString, ... */ }Corpus: Records · Record types · CA1067: Override Equals when implementing IEquatable
Prefer a record struct for a small immutable value
A readonly record struct keeps the value semantics without an allocation, which is the right shape for a key, a coordinate or an amount that is passed by value. A record class is the default when identity matters or the type is large.
Preferred
public readonly record struct Point(double X, double Y);Avoid
public record Point(double X, double Y); // one allocation per pointCorpus: Record types
Return a tuple for a private answer, a type for a public one
Tuples are unnamed beyond the method signature, so a caller can never add a member or a doc comment to them. Inside a type they are the cheapest way to return two values; across a public boundary they freeze the shape.
Preferred
public sealed record Range(int Start, int Length);
public Range Measure(ReadOnlySpan<char> text);Avoid
public (int Start, int Length) Measure(ReadOnlySpan<char> text);Corpus: Tuple types · Choosing between anonymous and tuple types
Deconstruct when the caller wants the parts
A Deconstruct method gives a type the same positional syntax a tuple has, so a caller can name what it needs without a member for every combination. Records with positional parameters get it generated.
Preferred
var (amount, currency) = price;Avoid
var amount = price.Amount;
var currency = price.Currency;Corpus: Deconstructing tuples and other types · Deconstruction
Use a discard for a value you deliberately ignore
A _ at the assignment says the value is not needed, and it works for out parameters and for parts of a tuple. A named but unused variable makes the next reader wonder whether the omission was a mistake.
Preferred
if (int.TryParse(text, out _))
{
Use(text);
}Avoid
if (int.TryParse(text, out var unused))
{
Use(text);
}Corpus: Discards