Skip to main content

πŸ“ 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
graph TD subgraph Class["class β†’ reference equality"] A1["a: {1, 2}"] -. "a == b?
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:

recordclass
Best forHolding data (DTOs, values, results)Objects with behavior and identity
EqualityBy value (contents)By reference (identity)
Default mutabilityImmutable (positional)Mutable
Free extrasToString, equality, with, deconstructNone (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 β€” a BankAccount, a Game, 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:

  1. Create a new project called Records.
  2. Declare a positional record Money(string Currency, decimal Amount).
  3. Create two separate Money values with the same currency and amount, and print whether they're equal (they should be True).
  4. Use a with-expression to make a copy with a different amount, and print both to show the original is unchanged.
  5. Deconstruct a Money value into currency and amount variables 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.