π Lesson 3.1: Records and Value Equality
Records are a concise way to declare types whose job is to hold data. In a single line you get value equality, a readable ToString, immutability, and easy copying β perfect for the data-carrying types you write all the time.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Declare a record, including the concise positional form
- Explain value equality and how it differs from a class's reference equality
- Use immutability and
with-expressions for safe copies - Deconstruct a record into its parts
- Decide when to use a record vs. a class
Estimated Time: 60 minutes
Project: Model data with records and see value equality in action.
In This Lesson
What Is a Record?
Many types exist just to bundle a few related values β a Point, a Product, an API response. Writing those as full classes means a lot of boilerplate: properties, a constructor, equality logic, a ToString. A record gives you all of that automatically.
π Definition
Record: a reference type declared with the record keyword, designed for holding data. Records come with built-in value equality, a helpful ToString, and support for immutability and easy copying.
Here's a class and the equivalent record. Both hold a name and age β but look at the difference in effort:
// As a class β lots of boilerplate
class PersonClass
{
public string Name { get; init; }
public int Age { get; init; }
public PersonClass(string name, int age) { Name = name; Age = age; }
}
// As a record β one line, and you get MORE (equality + ToString)
record PersonRecord(string Name, int Age);
Positional Records
That one-liner is a positional record. The parameters in parentheses become read-only properties automatically, and you get a matching constructor for free:
record Product(string Name, string Category, decimal Price);
Product keyboard = new Product("Keyboard", "Electronics", 45m);
Console.WriteLine(keyboard.Name); // Keyboard
Console.WriteLine(keyboard.Price); // 45
Records also generate a readable ToString out of the box β great for debugging:
Console.WriteLine(keyboard);
// Product { Name = Keyboard, Category = Electronics, Price = 45 }
π‘ The record you already met
This is exactly the Product and Person types from the LINQ lesson. Records are the idiomatic way to declare the small data types you query and pass around β which is why they show up constantly in modern C#.
Value vs. Reference Equality
This is the headline feature. With a normal class, two objects are "equal" only if they're the same object in memory β that's reference equality. Two separate class objects with identical data are considered different:
class PointClass
{
public int X { get; init; }
public int Y { get; init; }
}
var a = new PointClass { X = 1, Y = 2 };
var b = new PointClass { X = 1, Y = 2 };
Console.WriteLine(a == b); // False β different objects in memory
Console.WriteLine(a.Equals(b)); // False
A record instead compares by the values it holds β value equality. Two records with the same data are equal:
record PointRecord(int X, int Y);
var c = new PointRecord(1, 2);
var d = new PointRecord(1, 2);
Console.WriteLine(c == d); // True β same values!
Console.WriteLine(c.Equals(d)); // True
False" .- B1["b: {1, 2}"] end subgraph Record["record β value equality"] C1["c: (1, 2)"] == "c == d?
True" ==> D1["d: (1, 2)"] end style A1 fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style B1 fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style C1 fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px style D1 fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
β Why this matters
Value equality is exactly what you want for data. Two products with the same name, category, and price are the same product for most purposes. Records give you that behavior β plus a matching GetHashCode β automatically, so they work correctly as dictionary keys and in HashSet (Lesson 1.3) too.
Immutability and with
Positional record properties are immutable by default β once created, they can't be changed (they use init accessors, settable only during construction):
record Product(string Name, string Category, decimal Price);
Product p = new Product("Keyboard", "Electronics", 45m);
// p.Price = 50m; // β Error β records are immutable by default
Immutability is a feature: data that can't change under you is easier to reason about. But what if you need "the same product but cheaper"? Use a with-expression to create a modified copy, leaving the original untouched:
Product original = new Product("Keyboard", "Electronics", 45m);
Product discounted = original with { Price = 40m };
Console.WriteLine(original.Price); // 45 β unchanged
Console.WriteLine(discounted.Price); // 40 β a new copy with one change
π‘ Non-destructive mutation
The with expression copies all properties, then overrides only the ones you list. This "change by copying" style (called non-destructive mutation) is safer than editing shared objects β no one else's reference is affected.
Deconstruction
Positional records can be deconstructed β unpacked into separate variables in one step, mirroring how they were constructed:
record Point(int X, int Y);
Point p = new Point(3, 4);
var (x, y) = p; // deconstruct into x and y
Console.WriteLine($"x = {x}, y = {y}"); // x = 3, y = 4
π‘ Handy with tuples too
This same deconstruction syntax works with tuples and anywhere a type supports it. It's a clean way to pull multiple values out at once β and it pairs nicely with the pattern matching you'll learn in the next lesson.
Record vs. Class
Records don't replace classes β they're a better fit for a specific job. Here's how to choose:
| record | class | |
|---|---|---|
| Best for | Holding data (DTOs, values, results) | Objects with behavior and identity |
| Equality | By value (contents) | By reference (identity) |
| Default mutability | Immutable (positional) | Mutable |
| Free extras | ToString, equality, with, deconstruct | None (you write them) |
π‘ Rule of thumb: If a type is mostly a bundle of values and two instances with the same data should count as equal, use a record. If a type is about behavior and identity β aBankAccount, aGame, a service β use a class.
π‘ Records can do class-like things too
Records support methods, inheritance from other records, and even a non-positional form with regular property bodies. But their sweet spot β and the reason they exist β is concise, value-based data types. Start there.
Exercise & Quiz
ποΈ Exercise: Money with Records
Objective: Use a record's value equality, with-expression, and deconstruction.
Instructions:
- Create a new project called
Records. - Declare a positional record
Money(string Currency, decimal Amount). - Create two separate
Moneyvalues with the same currency and amount, and print whether they're equal (they should beTrue). - Use a
with-expression to make a copy with a different amount, and print both to show the original is unchanged. - Deconstruct a
Moneyvalue intocurrencyandamountvariables and print them.
Starter Code:
record Money(string Currency, decimal Amount);
// TODO: create two equal Money values and compare with ==
// TODO: use 'with' to make a copy with a new Amount
// TODO: deconstruct a Money into (currency, amount)
π‘ Hint
Compare with m1 == m2. Copy with var raised = original with { Amount = 100m };. Deconstruct with var (currency, amount) = original;.
β Solution
record Money(string Currency, decimal Amount);
var m1 = new Money("USD", 50m);
var m2 = new Money("USD", 50m);
Console.WriteLine(m1 == m2); // True β value equality
var raised = m1 with { Amount = 75m };
Console.WriteLine(m1); // Money { Currency = USD, Amount = 50 }
Console.WriteLine(raised); // Money { Currency = USD, Amount = 75 }
var (currency, amount) = m1;
Console.WriteLine($"{amount} {currency}"); // 50 USD
Output:
True
Money { Currency = USD, Amount = 50 }
Money { Currency = USD, Amount = 75 }
50 USD
π― Quick Quiz
Question 1: Two separate record instances with identical data areβ¦
Question 2: What does original with { Price = 40m } do?
Question 3: When is a record the better choice over a class?
Summary
π Key Takeaways
- A record is a concise, data-focused type; the positional form
record Product(string Name, decimal Price);generates properties, a constructor,ToString, and equality. - Records have value equality (equal if their contents match); classes have reference equality (equal only if the same object).
- Positional records are immutable; make modified copies with a
with-expression (non-destructive mutation). - Records can be deconstructed into their parts:
var (x, y) = point;. - Use a record for data where value equality fits; use a class for behavior-and-identity objects.
π Additional Resources
π What's Next?
Records pair beautifully with our next topic. In Lesson 3.2: Pattern Matching, you'll test and deconstruct values with expressive switch expressions and type patterns β turning tangled if chains into clear, concise code.
π Records unlocked!
You can now declare clean, value-based data types in a single line. Next: pattern matching.