Записи (F#)

Записи представляют собой простые агрегаты именованных значений, которые могут иметь элементы. Они могут быть структурами или ссылочными типами. По умолчанию они являются ссылочными типами.

Синтаксис

[ attributes ]
type [accessibility-modifier] typename =
    { [ mutable ] label1 : type1;
      [ mutable ] label2 : type2;
      ... }
    [ member-list ]

Замечания

В предыдущем синтаксисе имя типа — это имя типа записи, метка1 и метка2 — это имена значений, называемые метками, а type1 и type2 — типы этих значений. Список членов — это необязательный список элементов для типа. Атрибут можно использовать [<Struct>] для создания записи структуры, а не записи, которая является ссылочным типом.

Ниже приводятся некоторые примеры.

// Labels are separated by semicolons when defined on the same line.
type Point = { X: float; Y: float; Z: float }

// You can define labels on their own line with or without a semicolon.
type Customer =
    { First: string
      Last: string
      SSN: uint32
      AccountNumber: uint32 }

// A struct record.
[<Struct>]
type StructPoint = { X: float; Y: float; Z: float }

Если каждая метка находится в отдельной строке, точка с запятой является необязательной.

Значения можно задать в выражениях, известных как выражения записей. Компилятор вводит тип из используемых меток (если метки достаточно отличаются от других типов записей). Фигурные скобки ({ }) заключены в выражение записи. В следующем коде показано выражение записи, которое инициализирует запись с тремя элементами с плавающей запятой с метками xy и z.

let mypoint = { X = 1.0; Y = 1.0; Z = -1.0 }

Не используйте сокращенную форму, если может быть другой тип, который также имеет те же метки.

type Point = { X: float; Y: float; Z: float }
type Point3D = { X: float; Y: float; Z: float }
// Ambiguity: Point or Point3D?
let mypoint3D = { X = 1.0; Y = 1.0; Z = 0.0 }

Метки последнего объявленного типа имеют приоритет над метками ранее объявленного типа, поэтому в предыдущем примере mypoint3D выводится значение Point3D. Вы можете явно указать тип записи, как показано в следующем коде.

let myPoint1 = { Point.X = 1.0; Y = 1.0; Z = 0.0 }

Методы можно определить для типов записей так же, как и для типов классов.

Создание записей с помощью выражений записей

Записи можно инициализировать с помощью меток, определенных в записи. Выражение, которое делает это, называется выражением записи. Используйте фигурные скобки, чтобы заключить выражение записи и использовать точку с запятой в качестве разделителя.

В следующем примере показано, как создать запись.

type MyRecord = { X: int; Y: int; Z: int }

let myRecord1 = { X = 1; Y = 2; Z = 3 }

Точка с запятой после последнего поля в выражении записи и определении типа необязательна независимо от того, находятся ли поля в одной строке.

При создании записи необходимо указать значения для каждого поля. Нельзя ссылаться на значения других полей в выражении инициализации для любого поля.

В следующем коде тип myRecord2 выводится из имен полей. При необходимости можно явно указать имя типа.

let myRecord2 =
    { MyRecord.X = 1
      MyRecord.Y = 2
      MyRecord.Z = 3 }

Другая форма построения записей может оказаться полезной при копировании существующей записи и, возможно, изменении некоторых значений полей. В следующей строке кода показано следующее.

let myRecord3 = { myRecord2 with Y = 100; Z = 2 }

Эта форма выражения записи называется выражением копирования и обновления записи.

Записи неизменяемы по умолчанию; однако можно легко создавать измененные записи с помощью выражения копирования и обновления. Можно также явно указать изменяемое поле.

type Car =
    { Make: string
      Model: string
      mutable Odometer: int }

let myCar =
    { Make = "Fabrikam"
      Model = "Coupe"
      Odometer = 108112 }

myCar.Odometer <- myCar.Odometer + 21

Не используйте атрибут DefaultValue с полями записи. Лучше всего определить экземпляры записей по умолчанию с полями, которые инициализированы по умолчанию, а затем использовать выражение копирования и обновления записи для задания полей, отличающихся от значений по умолчанию.

// Rather than use [<DefaultValue>], define a default record.
type MyRecord =
    { Field1 : int
      Field2 : int }

let defaultRecord1 = { Field1 = 0; Field2 = 0 }
let defaultRecord2 = { Field1 = 1; Field2 = 25 }

// Use the with keyword to populate only a few chosen fields
// and leave the rest with default values.
let rr3 = { defaultRecord1 with Field2 = 42 }

Создание рекурсивных записей

Иногда при создании записи может потребоваться, чтобы она зависела от другого типа, который вы хотите определить после этого. Это ошибка компиляции, если вы не определите типы записей, которые будут взаимокурсивными.

Определение взаимно рекурсивных записей выполняется с and помощью ключевое слово. Это позволяет связать 2 или более типов записей вместе.

Например, следующий код определяет Person и Address тип как взаимно рекурсивный:

// Create a Person type and use the Address type that is not defined
type Person =
  { Name: string
    Age: int
    Address: Address }
// Define the Address type which is used in the Person record
and Address =
  { Line1: string
    Line2: string
    PostCode: string
    Occupant: Person }

Чтобы создать экземпляры обоих, сделайте следующее:

// Create a Person type and use the Address type that is not defined
let rec person =
  {
      Name = "Person name"
      Age = 12
      Address =
          {
              Line1 = "line 1"
              Line2 = "line 2"
              PostCode = "abc123"
              Occupant = person
          }
  }

Если бы вы определили предыдущий пример без and ключевое слово, то он не будет компилироваться. and Для взаимно рекурсивных определений требуется ключевое слово.

Сопоставление шаблонов с записями

Записи можно использовать с сопоставлением шаблонов. Можно явно указать некоторые поля и указать переменные для других полей, которые будут назначены при совпадении. Это показано в следующем примере кода.

type Point3D = { X: float; Y: float; Z: float }
let evaluatePoint (point: Point3D) =
    match point with
    | { X = 0.0; Y = 0.0; Z = 0.0 } -> printfn "Point is at the origin."
    | { X = xVal; Y = 0.0; Z = 0.0 } -> printfn "Point is on the x-axis. Value is %f." xVal
    | { X = 0.0; Y = yVal; Z = 0.0 } -> printfn "Point is on the y-axis. Value is %f." yVal
    | { X = 0.0; Y = 0.0; Z = zVal } -> printfn "Point is on the z-axis. Value is %f." zVal
    | { X = xVal; Y = yVal; Z = zVal } -> printfn "Point is at (%f, %f, %f)." xVal yVal zVal

evaluatePoint { X = 0.0; Y = 0.0; Z = 0.0 }
evaluatePoint { X = 100.0; Y = 0.0; Z = 0.0 }
evaluatePoint { X = 10.0; Y = 0.0; Z = -1.0 }

Выходные данные этого кода приведены следующим образом.

Point is at the origin.
Point is on the x-axis. Value is 100.000000.
Point is at (10.000000, 0.000000, -1.000000).

Записи и члены

Вы можете указать элементы в записях, как и классы. Нет поддержки полей. Распространенный подход заключается в определении статического элемента для простого Default построения записей:

type Person =
  { Name: string
    Age: int
    Address: string }

    static member Default =
        { Name = "Phillip"
          Age = 12
          Address = "123 happy fun street" }

let defaultPerson = Person.Default

Если вы используете идентификатор, этот идентификатор ссылается на экземпляр записи, член которой вызывается:

type Person =
  { Name: string
    Age: int
    Address: string }

    member this.WeirdToString() =
        this.Name + this.Address + string this.Age

let p = { Name = "a"; Age = 12; Address = "abc123" }
let weirdString = p.WeirdToString()

Различия между записями и классами

Поля записей отличаются от полей класса в том, что они автоматически предоставляются в качестве свойств, и они используются в создании и копировании записей. Запись строительства также отличается от строительства класса. В типе записи нельзя определить конструктор. Вместо этого применяется синтаксис конструкции, описанный в этом разделе. Классы не имеют прямой связи между параметрами конструктора, полями и свойствами.

Как и типы объединения и структуры, записи имеют семантику структурного равенства. Классы имеют семантику равенства ссылок. Это действие представлено в следующем примере кода:

type RecordTest = { X: int; Y: int }

let record1 = { X = 1; Y = 2 }
let record2 = { X = 1; Y = 2 }

if (record1 = record2) then
    printfn "The records are equal."
else
    printfn "The records are unequal."

Выходные данные этого кода приведены следующим образом:

The records are equal.

При написании одного кода с классами два объекта класса будут неравными, так как два значения будут представлять два объекта в куче, и будут сравниваться только адреса (если только тип класса не переопределяет System.Object.Equals метод).

Если требуется равенство ссылок для записей, добавьте атрибут [<ReferenceEquality>] над записью.

См. также