How it works
The explanation, with examples you can run.
Records
var a = new Money(10m, "EUR");var b = new Money(10m, "EUR");Console.WriteLine(a == b); // True: same valuesConsole.WriteLine(ReferenceEquals(a, b)); // False: two objectsvar c = a with { Amount = 25m };Console.WriteLine(c); // Money { Amount = 25, Currency = EUR } record Money(decimal Amount, string Currency);The positional properties are init-only, so a record is immutable unless you add settable properties yourself. That makes records good keys in dictionaries and good messages, DTOs and value objects. Two caveats: equality compares each property with its own Equals, so a record holding a List<T> compares the lists by reference; and a mutable property that changes after the record is used as a dictionary key breaks the lookup.
record struct gives the same features to a value type. A plain record struct has settable properties; readonly record struct is the immutable version.
Switch expressions and patterns
static string Describe(Shape s) => s switch{ Circle { Radius: <= 0 } => "invalid circle", Circle c => $"circle, area {Math.PI * c.Radius * c.Radius:F2}", Rect(var w, var h) when w == h => $"square {w}", Rect(var w, var h) => $"rect {w}x{h}", _ => "unknown",}; static string Classify(int[] xs) => xs switch{ [] => "empty", [var only] => $"one: {only}", [var first, .., var last] => $"from {first} to {last}",}; abstract record Shape;record Circle(double Radius) : Shape;record Rect(double W, double H) : Shape;With Circle(1), Rect(2, 2), Rect(2, 3) and [1, 2, 3], this printed circle, area 3.14, square 2, rect 2x3 and from 1 to 3 on .NET 10. The patterns used:
- type pattern
Circle c: matches and binds a typed variable; - property pattern
{ Radius: <= 0 }with a relational pattern inside; - positional pattern
Rect(var w, var h), which calls the generatedDeconstruct; - a case guard with
when; - list patterns (C# 11) with
..for "any number of elements"; - logical patterns
and,orandnot, as inis not nullor>= 0 and < 10.
Arms are tried in order, and the compiler reports an error if an arm can never be reached because an earlier one covers it, and a warning (CS8509) if some input matches no arm. That warning is the reason to prefer a switch expression over a chain of if statements: add a new kind of input and the compiler shows you every place that doesn't handle it yet. C# 14 can't declare that a hierarchy is closed, so a _ arm is still needed for an abstract base like Shape.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.