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.

C#
// 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);
C#
// 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());
C#
#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 error
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.