Modern C# · Lesson 1

Records and value equality

Describe a value with a record and compare its contents.

Choose equality that matches the data

A ticket in this example is described by a code and a price. Two independently constructed tickets can represent the same data even when they are different objects. Value equality makes that question explicit: the program compares the declared contents rather than asking whether two variables refer to one instance. The positional record declaration gives this small model generated equality and readable properties. Before using a record in a larger application, decide whether the model represents a value snapshot or an entity whose continuing identity matters even when its fields change.

Read the three output lines as separate claims

The sample constructs original with code A1 and price twelve, then constructs equal with the same two values. Their comparison prints True. The with expression creates revised with price fifteen while retaining the code. Printing the prices then produces 12 and 15. Those lines establish two different facts: the first comparison uses contents, and the update leaves the earlier ticket’s price available. Do not infer that the two initial objects are the same instance merely because their equality comparison succeeds.

var revised = original with { Price = 15m };

Separate assignment from a nondestructive update

A plain record declaration creates a record class unless another form is specified. Assigning its variable to another variable still copies an object reference. A with expression is the operation here that creates a modified record copy. That distinction prevents a common misunderstanding: records do not turn every assignment into a deep clone. In this sample the members are a string and a decimal, so their values provide a straightforward snapshot. A member pointing to mutable nested storage would introduce a separate ownership question that generated record syntax cannot decide for you.

Nested collections keep their own rules

Suppose a future ticket contains a mutable list of notes. A shallow copy can leave the original and revised records referring to the same list. Editing that list through either record could then affect both views, even though their scalar prices remain separate. Similarly, collection equality can be reference-based instead of element-by-element equality. Record equality follows the members' equality behavior; it does not invent a universal recursive comparison. Model the update and equality policies deliberately instead of assuming a short declaration guarantees complete immutability.

Practice: change one component at a time

First change the revised price back to twelve and add a comparison between original and revised. Predict why it should now be equal by contents even though the copy operation still created another record. Next change only the revised code to B2 while keeping the price twelve. Explain which component now makes equality fail. These controlled changes isolate the equality contract more clearly than modifying several fields at once, because every observed difference has one identifiable cause in the constructed data.

  • Keep a written table of code, price, and predicted equality for each ticket before running the variation.
  • Check that the earlier ticket still prints its original code and price after creating each revised value.

Practice: expose a shallow-copy boundary

As an independent exercise, add a list-valued notes member to a separate ticket variant. Create a copy with with, append a note through the copy, and inspect both lists. Then construct a version that deliberately copies the note collection before updating it. Compare storage independence separately from the records' equality results. The goal is not to memorize one cloning recipe, but to decide which values belong to a snapshot and which nested objects are intentionally shared between versions.

  • Use separate variables for shared-list and copied-list experiments so one mutation cannot contaminate the other case.
  • Do not change the original runnable example until you have saved the expected three-line result as a baseline.

Check the model rather than the keyword

A useful explanation of this program names the comparison rule, the copy operation, and the ownership boundary independently. The record syntax reduces repetitive implementation work, but callers still need a contract describing which data makes two tickets equal. When extending the model, ask whether each new member participates in identity, whether it can change after construction, and whether a revised value needs independent nested storage. Small examples remain valuable when their conclusions are stated precisely enough to survive those realistic extensions.

  • Expected baseline: True, then 12, then 15 on separate lines.
  • A with update preserves the old ticket; ordinary reference assignment is a different operation.
  • Generated equality does not promise deep copies or sequence equality for every possible member.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var original = new Ticket("A1", 12m);
var equal = new Ticket("A1", 12m);
var revised = original with { Price = 15m };
Console.WriteLine(original == equal);
Console.WriteLine(original.Price);
Console.WriteLine(revised.Price);
record Ticket(string Code, decimal Price);

Expected output

True
12
15
Try this
Use with when the old value should remain available.

Further reading