Skip to content

Chapter 3 of 7

Modern C# Language Features

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 and arrays for parsing.

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 with HasValue and Value members. The null-coalescing operator (??) returns the left operand if it is not null, otherwise the right; the null-conditional operator (?.) short-circuits the entire chain to null if any part is null, so person?.Address?.City is safe; and the null-coalescing assignment operator (??=) assigns only when the left side is null. Nullable reference types (C# 8) add compile-time null-safety analysis, with string meaning non-nullable and string? meaning nullable; the compiler warns on potential null dereferences. The null-forgiving operator (!) suppresses those warnings without runtime checks, so use it sparingly at trust boundaries. The throw expression (C# 7) lets throw appear inside expressions, including ternary operators, switch expressions, and null-coalescing, as in name ?? throw new ArgumentNullException(nameof(name)). The nameof operator returns the name of a symbol as a string with compile-time checking, and [CallerMemberName] auto-fills a parameter with the calling member's name, both used heavily in property change notifications. Verbatim strings (prefix @) disable escape processing and allow multi-line text, with "" for embedded quotes, and they can be combined with $ for interpolated verbatim strings. Raw string literals (C# 11) delimit multi-line text with three or more double quotes, allowing embedded quotes freely and using indentation rules based on the closing quote position. Preprocessor directives like #if, #nullable, #pragma, #region, and #error are processed by the compiler rather than a separate preprocessor.

All chapters
  1. 1Type System and Language Fundamentals
  2. 2Functions, Delegates, and Events
  3. 3Modern C# Language Features
  4. 4LINQ and Data Querying
  5. 5Asynchronous and Parallel Programming
  6. 6Memory, Performance, and Strings
  7. 7Collections, Concurrency, Resources, and Diagnostics

Drill it

Reading is not remembering. These come from the Csharp Programming deck:

Q

What is LINQ in C#?

LINQ (Language Integrated Query) is a set of features that allows you to write query expressions directly in C# to filter, sort, and transform data from collect...

Q

What is the difference between LINQ query syntax and method syntax?

Query syntax resembles SQL:var q = from x in list where x > 5 select x;Method syntax uses extension methods:var q = list.Where(x => x > 5);Both compile to the s...

Q

What does the LINQ Select method do?

Select projects each element of a sequence into a new form (a transformation/map operation).var names = people.Select(p => p.Name);It returns an IEnumerable&lt;...

Q

What is deferred execution in LINQ?

Deferred execution means a LINQ query is not executed when defined, but only when the results are enumerated (e.g., via foreach, ToList(), or Count()). This all...