# Records

> A `record` is a class whose values compare by their MEMBERS rather than by identity, exactly as a C# record does. The positional form `record Person(string Name, int Age);` declares the members, a constructor that takes them and a `Deconstruct`; every record gets `==`, `Equals`, `GetHashCode` over its members, `with`, and a `ToString` that lists them. Reach for one when two separately built values with the same contents should be the same value.

<!-- id: class-records · area: class · stability: preview · html: https://osysharp.com/reference/class/records/ -->

## Summary        {#summary}
A positional record, compared, copied and taken apart:

```osy title="a positional record" test app=class-records
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      {#signature}
```osy syntax
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 class
```

## Description    {#description}

### What a positional record declares   {#positional}
`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`, so `var (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   {#members}
- **Value equality.** `==`, `!=` and `Equals` compare every member ([[class-equality#field-comparison]]), and
  `GetHashCode` agrees with them — so `Distinct()`, `Contains`, `IndexOf` and 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](https://osysharp.com/reference/class/with/)).
- **`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}
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](https://osysharp.com/reference/entity/declaration/) for
anything stored.

### A record struct   {#record-struct}
`record struct Pos(int X, int Y);` is a [struct](https://osysharp.com/reference/class/structs/) 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       {#examples}

```osy title="a record in a list: one element per value" test app=class-records-list
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
}
```

```osy title="a record with a body" test app=class-records-body
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         {#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       {#see-also}
- [Comparing classes](https://osysharp.com/reference/class/equality/) — identity for a class, members for a record
- [Structs](https://osysharp.com/reference/class/structs/) — a value type, and the `record struct`
- [Copying a class with changes](https://osysharp.com/reference/class/with/) — copying a value with some members replaced
- [Classes](https://osysharp.com/reference/class/index/) — classes, and when to use one
