Records

One line gives you an immutable type with value equality, a readable ToString and deconstruction, replacing about 40 lines by hand.

Syntaxpublic record Person(string FirstName, string LastName);

A record is a class or a struct that the compiler fills in for you. You write the data it carries; the compiler writes the members that almost every data-carrying type needs.

What the compiler writes

From public record Person(string FirstName, string LastName); you get a public init-only property for each parameter in the brackets, a constructor taking them in order, a ToString that prints Person { FirstName = Ada, LastName = Lovelace }, an Equals and GetHashCode that compare property against property, the == and != operators, a Deconstruct method, and the copying machinery behind the with expression. The parameters in brackets are called positional parameters.

Value equality

Two plain class objects are equal only when they are the same object in memory. Two records are equal when they are the same type and hold the same values. That is the change people notice first.

record class and record struct

record and record class mean the same thing and give a reference type. record struct, added in C# 10, gives a value type; its positional properties are read-write unless you write readonly record struct. Records arrived in C# 9.

What trips people up

Immutability is shallow. A record holding a string[] cannot be given a different array, but the array's contents can still be changed, and Equals compares the array by reference, not element by element. A record can inherit from a record but never from a plain class, and equality compares runtime types, so a Circle is never equal to a Square even with identical values.

Example

Example · csharp
Person a = new Person("Ada", "Lovelace");
Person b = new Person("Ada", "Lovelace");

Console.WriteLine(a);                                 // generated ToString
Console.WriteLine(a == b);                            // generated ==
Console.WriteLine(object.ReferenceEquals(a, b));      // still two objects
Console.WriteLine(a.GetHashCode() == b.GetHashCode());

// Generated Deconstruct, one out parameter per positional parameter.
var (first, last) = a;
Console.WriteLine($"{last}, {first}");

// One line. The properties, the constructor, ToString, Equals, GetHashCode,
// ==, != and Deconstruct are all written by the compiler.
record Person(string FirstName, string LastName);

// Output:
// Person { FirstName = Ada, LastName = Lovelace }
// True
// False
// True
// Lovelace, Ada

When to use it

  • Define the request and response shapes of an HTTP API, where two identical payloads should compare equal.
  • Represent a domain value such as an address or a date range that should never change once created.
  • Write a test assertion comparing a whole returned object in one line instead of field by field.
  • Use a multi-field value as a Dictionary key without hand-writing Equals and GetHashCode.

More examples

The longhand the record replaces

The shorthand and the longhand are the same type. Read this once so you recognise both forms in a codebase, then stop writing the long one. It also shows what you give up control of: if you need a different hash strategy, a custom ToString, or looser equality, you can declare just that member inside a record and the compiler will not generate its own version of it.

Example · csharp
Person a = new Person("Ada", "Lovelace");
Person b = new Person("Ada", "Lovelace");

Console.WriteLine(a);
Console.WriteLine(a == b);

// Everything below is what `record Person(string FirstName, string LastName);`
// generates. Same behaviour, written out by hand.
sealed class Person : IEquatable<Person>
{
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public string FirstName { get; init; }
    public string LastName { get; init; }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }

    public bool Equals(Person? other) =>
        other is not null && FirstName == other.FirstName && LastName == other.LastName;

    public override bool Equals(object? obj) => Equals(obj as Person);

    public override int GetHashCode() => HashCode.Combine(FirstName, LastName);

    public static bool operator ==(Person? left, Person? right) =>
        left is null ? right is null : left.Equals(right);

    public static bool operator !=(Person? left, Person? right) => !(left == right);

    public override string ToString() =>
        $"Person {{ FirstName = {FirstName}, LastName = {LastName} }}";
}

// Output:
// Person { FirstName = Ada, LastName = Lovelace }
// True

record struct: value equality on a value type

Reach for a record struct when the type is small, carries data, and is compared or hashed often. A plain struct already has value equality, but its implementation lives in ValueType.Equals and uses reflection; a record struct gets a compiler-generated version that reads the declared members directly, so it is markedly faster. Prefer readonly record struct unless you genuinely need to assign to the properties, because a mutable value type copied everywhere is easy to get wrong.

Example · csharp
Pixel origin = new Pixel(0, 0);
Pixel moved = origin with { X = 4 };

Console.WriteLine(origin);
Console.WriteLine(moved);
Console.WriteLine(origin == new Pixel(0, 0));

// A plain record struct has read-write properties, not init-only ones.
Tally tally = new Tally(1);
tally.Count = 2;
Console.WriteLine(tally);

// Added in C# 10. readonly makes the positional properties init-only.
readonly record struct Pixel(int X, int Y);

record struct Tally(int Count);

// Output:
// Pixel { X = 0, Y = 0 }
// Pixel { X = 4, Y = 0 }
// True
// Tally { Count = 2 }

Records in an inheritance hierarchy

The generated equality checks the runtime type first, through a hidden property called EqualityContract, so two records in the same hierarchy with identical data are still unequal if their types differ. That is usually what you want and it is the opposite of what a hand-written Equals comparing only fields would do. A derived record repeats the base record's positional parameters in its own list and passes them up; it does not redeclare those properties.

Example · csharp
Shape circle = new Circle(2);
Shape square = new Square(2);

Console.WriteLine(circle);
Console.WriteLine(circle == new Circle(2));

// Same declared type, same Size, different runtime type.
Console.WriteLine(circle.Equals(square));

// A record may inherit from a record. It may not inherit from a class,
// and a class may not inherit from a record.
abstract record Shape(int Size);

record Circle(int Size) : Shape(Size);

record Square(int Size) : Shape(Size);

// Output:
// Circle { Size = 2 }
// True
// False

Discussion

  • Be the first to comment on this lesson.