Записи (F#)

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

Синтаксис

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

Remarks

В предыдущем синтаксисе typename — это имя типа записи, label1 и label2 — это имена значений, которые называются метками, а type1 и type2 типами этих значений. Member-list — это необязательный список элементов для типа. Атрибут можно использовать для [<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>] над записью.

См. также раздел