Modern C# has steadily added features that improve expressiveness and reduce boilerplate. Pattern matching tests a value against a pattern and extracts information, with common forms including type patterns (obj is string s), constant patterns (x is 42), and property patterns (p is { Age: > 18 }). C# 8 introduced switch expressions that use => to map patterns to values, with _ as the discard pattern serving as a default. C# 9 added relational and logical patterns so you can write score switch { >= 90 => "A", >= 80 and < 90 => "B", _ => "C" } using and, or, and not combinators. List patterns (C# 11) match sequences: [1, 2, ..] matches a sequence starting with 1 and 2, [_, _, _] matches exactly three elements, and [] matches the empty sequence, with the .. slice pattern matching zero or more elements. These work especially well with Span
Records (C# 9) are reference types designed for immutable data with value-based equality, and record structs (C# 10) are the value-type equivalent, allocated on the stack with the same equality and with expression semantics. A positional record like public record Person(string Name, int Age) auto-generates Equals, GetHashCode, ToString, and a Deconstruct method. The with expression creates a copy with selected properties changed, leaving the original intact. Init-only setters (C# 9) allow properties to be set only during object initialization, making them effectively immutable thereafter. The required modifier (C# 11) forces callers to set those properties at construction time, eliminating a class of bugs around unset state. C# 12 introduced primary constructors, which let you declare constructor parameters directly on the type, automatically making them available throughout the class body and removing the boilerplate of explicit field declaration. Tuples (ValueTuple) are lightweight value-type groupings, and deconstruction assigns tuple elements to individual variables, also available on custom types that implement a Deconstruct method.
C# 12 introduced collection expressions that unify array, list, and span literals: int[] arr = [1, 2, 3] works for any compatible target, and the spread operator .. combines collections, so int[] combined = [..arr, ..other] concatenates them. Index from end (^n) and range (a..b) operators enable concise slicing: arr[^1] is the last element, arr[1..3] is a slice, and arr[^2..] is the last two; types support these by exposing an int indexer and a Range indexer. Top-level statements let you write Program.cs without the class Program { static void Main() } boilerplate, with the compiler synthesizing a Main behind the scenes; only one file in a project may use them. Global using directives apply a using statement to every file in the project, and .NET 6+ projects auto-emit common ones like System, System.Collections.Generic, System.IO, and System.Linq.
Nullable value types let value types represent null, so int? age = null is shorthand for Nullable