Summary#
A positional record, compared, copied and taken apart:
record Person(string Name, int Age);
string Describe() {
var a = new Person("Ada", 36);
var b = new Person("Ada", 36);
var older = a with { Age = 37 };
var (name, age) = older;
return $"{a == b} {name} {age} {a}"; // "True Ada 37 Person { Name = Ada, Age = 36 }"
}Signature#
record Name(Type A, Type B = default); // positional: A and B are init-only properties
record Name(Type A) { /* more members */ } // positional, with a body
record Name { public Type A { get; init; } } // members declared the ordinary way
record Name(Type A) : Base(A); // derived: the arguments go to the base's constructor
sealed record Name(…); public record Name(…); // the class modifiers, as on a classDescription#
What a positional record declares#
record Person(string Name, int Age); is the class C# compiles it to:
- a property per parameter,
public string Name { get; init; }— set when the record is made, and not after; - a constructor taking the parameters in order,
new Person("Ada", 36); Deconstruct, sovar (name, age) = person;takes it apart.
A parameter may have a default (record Page(int Size = 20);). Any other member goes in a body, exactly as on a
class.
What every record gets#
- Value equality.
==,!=andEqualscompare every member ([[class-equality#field-comparison]]), andGetHashCodeagrees with them — soDistinct(),Contains,IndexOfand a dictionary key treat two equal records as one. Two records of different types are never equal. with.person with { Age = 37 }is a copy with those members replaced (Copying a class with changes).ToString.Person { Name = Ada, Age = 36 }— the type's name and each member in declaration order, as C# writes it. Override it (public override string ToString() => Name;) to say something else.
When to use one#
When the VALUE is what matters and two equal ones are interchangeable: a money amount, a date range, a column of a
grid, a filter in a list. A value rebuilt on every render still equals the one you kept, because a record compares
what it holds. Keep a plain class for a thing with an identity of its own, and an entity for
anything stored.
A record struct#
record struct Pos(int X, int Y); is a struct with a record's members — copied where it is stored,
and compared with == (a plain struct has no ==). Its positional members can be set ({ get; set; }); write
readonly record struct to make them init-only.
Examples#
record Tag(string Label);
int Distinct() {
var tags = new List<Tag> { new Tag("red"), new Tag("blue"), new Tag("red") };
return tags.Distinct().Count(); // 2
}record Range(int Low, int High) {
public int Width => High - Low;
public bool Contains(int n) { return n >= Low && n <= High; }
}
int Measure() {
var r = new Range(3, 10);
return r.Contains(5) ? r.Width : 0; // 7
}Errors#
| Message | Cause | Fix |
|---|---|---|
'Person.Name' is init-only | A positional member was assigned after the record was made. | Make a copy: p with { Name = "b" }. |
an attribute on a positional parameter of 'X' is not supported yet | [property: …] on a positional parameter. | Declare that member in the record's body, with its attributes. |
See also#
- Comparing classes — identity for a class, members for a record
- Structs — a value type, and the
record struct - Copying a class with changes — copying a value with some members replaced
- Classes — classes, and when to use one