Записи (records)

Записи — именованные неизменяемые «корзинки» данных, основа моделирования домена в F#.

Запись (record) — тип с именованными полями; в F# записи по умолчанию неизменяемы и сравниваются по значению (структурное равенство).

Объявление и создание

В отличие от кортежа, у записи поля имеют имена — код самодокументируется.

type Person = {
    Name: string
    Age: int
}

let alice = { Name = "Алиса"; Age = 30 }
printfn "%s, %d" alice.Name alice.Age

Вывод:

Алиса, 30

Тип Person часто даже не нужно указывать при создании — F# выводит его по набору полей.

Неизменяемость и копирование с изменением

Поля записи неизменяемы. Чтобы «поменять» поле, создают копию с изменением через with — старая запись остаётся нетронутой.

type Person = { Name: string; Age: int }
let alice = { Name = "Алиса"; Age = 30 }
let older = { alice with Age = 31 }
printfn "%d -> %d" alice.Age older.Age

Вывод:

30 -> 31

Это паттерн функционального обновления: вместо мутации создаётся новое значение. Так данные остаются предсказуемыми.

Структурное равенство

Две записи равны, если равны все их поля — без переопределения equals вручную, как в C#.

type Point = { X: int; Y: int }
let a = { X = 1; Y = 2 }
let b = { X = 1; Y = 2 }
printfn "%b" (a = b)

Вывод:

true

Записи в сопоставлении

Поля записи можно разбирать в match.

type Person = { Name: string; Age: int }
let greet p =
    match p with
    | { Age = 0 } -> "новорождённый"
    | { Name = n; Age = a } -> sprintf "%s, %d лет" n a
printfn "%s" (greet { Name = "Боб"; Age = 25 })

Вывод:

Боб, 25 лет

Как работает под капотом

Запись компилируется в .NET-класс с приватными полями и свойствами только для чтения. Компилятор автоматически генерирует структурное Equals, GetHashCode и ToString. Конструкция with создаёт новый объект, копируя неизменённые поля и подставляя новые — это поверхностное копирование, поэтому оно дёшево, а исходная запись не страдает.

Частые ошибки

  • Пытаться присвоить полю записи новое значение — поля неизменяемы, нужен with.
  • Путать записи и классы: запись даёт структурное равенство и краткий синтаксис «из коробки».
  • Думать, что with мутирует исходную запись — он создаёт копию.

Итоги

  • Запись — тип с именованными полями, неизменяемый по умолчанию.
  • «Изменение» — это копия через { запись with Поле = значение }.
  • Записи сравниваются по значению (структурное равенство) автоматически.
  • Поля записи можно разбирать в сопоставлении с образцом.
Проверьте себя
1. Чем запись отличается от кортежа?
AНичем
BУ полей записи есть имена
CЗапись изменяема
DКортеж сравнивается по значению, а запись нет
2. Как «изменить» поле неизменяемой записи?
AПрисвоить через <-
BСоздать копию через { запись with Поле = значение }
CИспользовать mutate()
DНикак
3. Как сравниваются две записи?
AПо ссылке
BПо значению (структурное равенство) автоматически
CНельзя сравнивать
DТолько после переопределения Equals