Records & Pattern Matching

Course 2 · Ch 5
Records & Pattern Matching
C# 9 (2020) — a rare case where C# genuinely arrived before java2-7's own Java 16 records

csharp1-4 previewed init-only properties as the mechanism this chapter builds on. This is where that promise pays off in full — real record syntax, and a capability neither Java's nor even Kotlin's own records quite match.

record — Full Syntax

public record Point(int X, int Y); // one line — a positional record Point p = new Point(3, 4); p.X; // 3 — a real property, get; init; p.ToString(); // "Point { X = 3, Y = 4 }" (int x, int y) = p; // deconstruction — free, no extra code

A one-line positional record generates even more than csharp1-4's own hand-assembled init properties: a constructor, get; init; properties for each component, correct Equals()/GetHashCode()/ToString(), and a Deconstruct method enabling tuple-style unpacking — all from one line.

Genuinely Comparable to java2-7's Records — But Arriving First

C# 9 (2020) shipped record; java2-7's own Java records arrived in Java 16 (2021) — a rare case on this site where C# genuinely got there first, worth naming plainly given how often the comparison runs the other way. Both generate equality, hashing, and string formatting automatically. C#'s positional syntax is even more compact than Java's own record header syntax, which still requires listing each component explicitly in a similar but slightly more verbose form.

with-Expressions — Non-Destructive Mutation

Point p1 = new Point(3, 4); Point p2 = p1 with { X = 99 }; // a NEW record — Y copied unchanged, X replaced p1.X; // still 3 — p1 is untouched p2.X; // 99

Here's the real reveal: with creates a brand-new record instance with just the named properties changed, copying everything else — something java2-7's own Java records have no built-in equivalent for at all. Kotlin's data class is the actual match here, via its own copy() method — a case where C# converges with Kotlin rather than with Java.

record class vs. record struct

public record class Customer(string Name); // reference type — the default, "record class" == "record" public record struct Coordinate(double Lat, double Lng); // value type (C# 10)

C# 10 let a record be either a reference type (record class, the plain record keyword's default) or a genuine value type (record struct) — layering the record's free boilerplate directly on top of csharp1-2's own struct/class split. This is capability neither Java's nor Kotlin's records offer at all, since neither language has C#'s own value/reference type distinction to apply it to in the first place.

Pattern Matching, Building on csharp1-3's Light Touch

string Describe(Point p) => p switch { { X: 0, Y: 0 } => "origin", // property pattern ( 0, _ ) => "on the Y axis", // positional pattern — uses Deconstruct directly var pt when pt.X > 0 and pt.Y > 0 => "first quadrant", // and-combinator _ => "elsewhere" };

Property patterns ({ X: 0, Y: 0 }), positional patterns (using the record's own Deconstruct directly), and the and/or/not combinators are a genuinely rich pattern language — well beyond java1-3's own simpler type-pattern-plus-switch-expression combination, and a natural extension of the type patterns java1-3 already introduced.

AspectJava records (java2-7)Kotlin data classC# record
ArrivalJava 16 (2021)Kotlin 1.0C# 9 (2020) — first vs. Java
Non-destructive copy with one field changedno built-in equivalentcopy() methodwith { ... } expression
Value-type optionnot applicable — Java has no value/reference splitnot applicablerecord struct (C# 10)
Use with-expressions instead of hand-writing a new constructor call
p1 with { X = 99 } reads directly as "the same record, except X" — cleaner and less error-prone than manually re-listing every unchanged property in a fresh constructor call.
with performs a shallow copy
A record containing a mutable reference-type field — a List<T>, for instance — shares that exact same list between the original and the with-modified copy. Mutating the list through either instance affects both, since with only copies the reference, not the referenced object itself.

Coding Challenges

Challenge 1

Define a positional record Product(string Name, decimal Price), create an instance, print its ToString() output, and deconstruct it into two separate variables.

📄 View solution
Challenge 2

Using Challenge 1's Product record, create an instance, use a with-expression to produce a second instance with only the Price changed, and print both to show the original is unaffected.

📄 View solution
Challenge 3

Define a record Point(int X, int Y) and write a switch expression using a property pattern for the origin, a positional pattern for points on the X axis, and a discard for everything else. Test it against three different points.

📄 View solution

Chapter 5 Quick Reference

  • A one-line positional record generates a constructor, get;init; properties, Equals/GetHashCode/ToString, and Deconstruct
  • C# records (C# 9, 2020) genuinely shipped before java2-7's own Java 16 (2021) records — a rare "C# first" case
  • with-expressions create a modified copy with named properties changed — no Java record equivalent, but a real match with Kotlin's data class copy()
  • record struct (C# 10) makes a record a value type — capability neither Java nor Kotlin records offer, since neither has C#'s value/reference split
  • Property, positional, and and/or/not pattern combinators extend java1-3's own simpler type patterns considerably
  • with is a shallow copy — mutable reference fields are shared between original and copy
  • Next chapter: nullable reference types — a three-way comparison against Kotlin's built-in null safety and Java's total lack of either