Lesson 11 of 13
Records, Pattern Matching & Nullable Types
Learn records, pattern matching and nullable types, three modern C# features that cut boilerplate and catch at compile time.
// Records: immutable value objects with equality, ToString and deconstruction
public record Money(decimal Amount, string Currency)
{
// Validation runs in the primary constructor body
public decimal Amount { get; init; } = Amount >= 0
? Amount
: throw new ArgumentOutOfRangeException(nameof(Amount));
}
public record Address(string Street, string City, string Zip);
public record Customer(int Id, string Name, Address Home);
var a = new Money(10m, "EUR");
var b = new Money(10m, "EUR");
Console.WriteLine(a == b); // True — value equality, not reference
Console.WriteLine(a); // Money { Amount = 10, Currency = EUR }
// "with" makes a modified copy, leaving the original untouched
var customer = new Customer(1, "Ada", new Address("1 Main St", "Lisbon", "1000"));
var moved = customer with { Home = customer.Home with { City = "Porto" } };
// Deconstruction
var (id, name, _) = customer;
Console.WriteLine($"{id} {name}");
// record struct for small value types (no heap allocation)
public readonly record struct Point(double X, double Y);// Pattern matching: switch expressions replace long if/else ladders.
abstract record Shape;
record Circle(double Radius) : Shape;
record Rect(double W, double H) : Shape;
record Triangle(double Base, double Height) : Shape;
static double Area(Shape shape) => shape switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rect { W: var w, H: var h } => w * h,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentException("unknown shape"),
};
// Property, relational and logical patterns
static string Describe(object o) => o switch
{
int n when n < 0 => "negative",
int and > 100 => "big number",
int => "number",
string { Length: 0 } => "empty string",
string s => $"string of {s.Length}",
Circle { Radius: > 10 } => "big circle",
null => "nothing",
_ => "something else",
};
// Positional patterns deconstruct records inline
static string Quadrant(Point p) => p switch
{
(0, 0) => "origin",
( > 0, > 0) => "I",
( < 0, > 0) => "II",
_ => "elsewhere",
};
// is patterns in conditions
if (o is string { Length: > 3 } text) Console.WriteLine(text.ToUpper());#nullable enable // on by default in new projects
// The ? is now part of the type: string vs string?
string definitelyThere = "hi";
string? maybeMissing = null;
// Console.WriteLine(maybeMissing.Length); // warning: possible null dereference
if (maybeMissing is not null)
Console.WriteLine(maybeMissing.Length); // fine — the compiler narrowed it
// Null-handling operators
string display = maybeMissing ?? "(none)"; // null-coalescing
maybeMissing ??= "default"; // assign only if null
int? len = maybeMissing?.Length; // null-conditional
string forced = maybeMissing!; // "trust me" — use sparingly
// Guard clauses
static void Send(string? email)
{
ArgumentException.ThrowIfNullOrWhiteSpace(email); // .NET 8+
Console.WriteLine(email.ToUpper()); // no warning after the guard
}
// required members must be set at construction
public class Config
{
public required string ConnectionString { get; init; }
public int Timeout { get; init; } = 30;
}
var cfg = new Config { ConnectionString = "..." }; // omitting it is a compile errorFinished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.