记录 (F#)

记录表示已命名值的简单聚合,可选择包含成员。 它们可以是结构或引用类型。 默认情况下,它们是引用类型。

语法

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

备注

在上面的语法中,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 }

当每个标签在单独的行上时,分号是可选的。

可在称为“记录表达式”的表达式中设置值。 如果标签与其他记录类型中的标签明显不同,编译器会从使用的标签中推断类型。 大括号 ({ }) 将记录表达式括起来。 下面的代码显示了一个记录表达式,它使用 3 个具有标签 xyz 的浮动元素初始化为一个记录。

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 个或更多记录类型链接在一起。

例如,以下代码将 PersonAddress 类型定义为相互递归:

// 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>] 属性。

另请参阅