S10.0 What This Teaches
- Record class vs plain class: value equality by default
- Positional records and primary constructors
- Non-destructive mutation with
withexpressions init-only properties- Record structs for stack-allocated value types
- Inheritance and pattern matching with records
S10.1 Record Types
record is a reference type where equality is based on property
values, not object identity. The compiler generates Equals,
GetHashCode, ToString, and ==:
// Records - value equality on reference types.
record Person(string Name, int Age);
var alice1 = new Person("Alice", 30);
var alice2 = new Person("Alice", 30);
var bob = new Person("Bob", 25);
Console.WriteLine(alice1 == alice2); // True - same values
Console.WriteLine(alice1 == bob); // False
Console.WriteLine(alice1); // Person { Name = Alice, Age = 30 }
== checks reference identity
by default. Use records when you want data containers with structural equality.
S10.2 Positional Records
record T(Type Prop, ...) declares a primary
constructor and generates matching init-only properties. You can
still add extra members:
record Point(double X, double Y)
{
// Computed property - not part of equality
public double Length => Math.Sqrt(X * X + Y * Y);
// Additional constructor
public Point(double v) : this(v, v) { }
}
var p = new Point(3, 4);
Console.WriteLine(p.Length); // 5
// Deconstruct into variables
var (x, y) = p;
Console.WriteLine($"x={x}, y={y}");
S10.3 Non-Destructive Mutation with with
with expression creates a
copy with specified properties changed - the original is unchanged:
record Address(string Street, string City, string Zip);
var home = new Address("123 Main St", "Springfield", "62701");
// Change only the city; everything else is copied
var relocated = home with { City = "Shelbyville", Zip = "62702" };
Console.WriteLine(home); // Address { Street = 123 Main St, City = Springfield, Zip = 62701 }
Console.WriteLine(relocated); // Address { Street = 123 Main St, City = Shelbyville, Zip = 62702 }
Console.WriteLine(home == relocated); // False
with is the idiomatic way to model change in immutable data.
It avoids accidental mutation while remaining concise.
S10.4 init-only Properties
init accessor allows a property to be set during object
initialization but not afterward. You can combine this with object initializers
on non-positional records:
record Config
{
public string Host { get; init; } = "localhost";
public int Port { get; init; } = 8080;
public bool Tls { get; init; } = false;
}
var cfg = new Config { Host = "prod.example.com", Port = 443, Tls = true };
Console.WriteLine(cfg);
// cfg.Port = 9090; // compile error - init-only after construction
// Non-positional records still support with
var dev = cfg with { Host = "dev.example.com", Tls = false };
S10.5 Record Structs
record struct gives value-type semantics (stack allocation, copy
on assignment) while keeping the auto-generated equality and ToString:
record struct Vector2(double X, double Y)
{
public static Vector2 operator +(Vector2 a, Vector2 b) => new(a.X + b.X, a.Y + b.Y);
public double Length => Math.Sqrt(X * X + Y * Y);
}
var v1 = new Vector2(1, 2);
var v2 = new Vector2(3, 4);
Console.WriteLine(v1 + v2); // Vector2 { X = 4, Y = 6 }
Console.WriteLine(v1 == v2); // False
// readonly record struct - all properties immutable
readonly record struct Color(byte R, byte G, byte B);
record struct for small data (2-4 fields) that is passed
by value and compared by value - coordinates, colors, keys.
S10.6 Inheritance and Pattern Matching
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
record Triangle(double Base, double Height) : Shape;
static double Area(Shape s) => s switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
Triangle t => 0.5 * t.Base * t.Height,
_ => throw new ArgumentException("unknown shape")
};
Console.WriteLine(Area(new Circle(5))); // 78.54
Console.WriteLine(Area(new Rectangle(4, 6))); // 24
Console.WriteLine(Area(new Triangle(3, 8))); // 12
S10.7 Example - All Together
// Records - immutable order pipeline with with-expressions.
record Product(string Name, decimal Price);
record OrderLine(Product Item, int Qty)
{
public decimal Total => Item.Price * Qty;
}
record Order(string Customer, IReadOnlyList<OrderLine> Lines)
{
public decimal GrandTotal => Lines.Sum(l => l.Total);
}
var widget = new Product("Widget", 9.99m);
var gadget = new Product("Gadget", 49.95m);
var order = new Order("Alice", new[]
{
new OrderLine(widget, 3),
new OrderLine(gadget, 1),
});
Console.WriteLine($"Customer: {order.Customer}");
foreach (var line in order.Lines)
Console.WriteLine($" {line.Item.Name} x{line.Qty} = {line.Total:C}");
Console.WriteLine($"Total: {order.GrandTotal:C}");
// Update price without mutating the original
var discountedWidget = widget with { Price = 7.99m };
Console.WriteLine($"Sale price: {discountedWidget.Price:C}");
S10.8 Exercise
Exercise
- Define a
record Temperature(double Value, string Unit). Add a methodToCelsius()that converts Fahrenheit and Kelvin to Celsius. Usewithto produce a Celsius version of a given temperature. - Model a simple event system: define an abstract
record Eventwith derived recordsUserCreated,UserDeleted, andEmailChanged. Write aDescribe(Event e)method using a switch expression. - Use
readonly record structto model an RGB color. Add a methodBlend(Color other)that averages the two colors. Verify value equality works correctly.
S10.9 Common Mistakes
Mutable properties break the value equality contract
record BadPoint(double X, double Y)
{
public double Z { get; set; } // mutable - excluded from equality!
}
var a = new BadPoint(1, 2) { Z = 3 };
var b = new BadPoint(1, 2) { Z = 9 };
Console.WriteLine(a == b); // True - Z is not in equality check
init-only properties are included
in auto-generated equality. Mutable set properties are silently excluded.
Confusing record class and record struct
record (or record class) is a reference type - two
variables can point to the same object. record struct is a value
type - assigning copies the data. Choose based on size and usage pattern,
not just on needing value equality.
S10.10 Key Terms
| Term | Meaning |
|---|---|
| record | Reference type with compiler-generated value equality, ToString, and deconstruct |
| positional record | record T(Type Prop, ...) shorthand generating primary constructor and init properties |
| with expression | Creates a copy of a record with specified properties changed; original unchanged |
| init accessor | Property setter that can only be called during object initialization |
| record struct | Value-type record; stack-allocated, copied on assignment |
| readonly record struct | record struct where all properties are immutable after construction |
| value equality | Equality based on property values, not object identity |
| deconstruct | Positional records generate Deconstruct() enabling var (x, y) = point syntax |