记录分布运算符

若要在不合并不同的模型类型的情况下共享字段,请使用记录分布,从 F# 11 开始可用。

用于 ...Type 向新的命名记录贡献字段声明,或 ...expression 向命名或匿名记录贡献字段值。 声明重用和值映射是独立的选择。

撰写记录类型

若要跨配置类型共享连接字段,请分散 Endpoint 到新的记录声明中:

type Endpoint = { Host: string; Port: int }

type ClientOptions =
    { ...Endpoint
      TimeoutSeconds: int }

ClientOptions has Host、 Port和 TimeoutSeconds. 它是一种不同的名义类型,而不是子 Endpoint 类型。

多个源和显式声明可以出现在同一类型定义中:

type Logging = { LogLevel: string }
type LoggedClientOptions = { ...ClientOptions; ...Logging }

仅记录字段参与。 不会复制非记录字段和静态成员的方法、属性。 源可以是命名记录或匿名记录类型,包括匿名记录的缩写。 类型分布的目标必须是命名记录类型。

撰写记录值

将终结点复制到客户端选项中,然后使用匿名记录替换主机:

let local = { Host = "localhost"; Port = 5432 }
let options: ClientOptions = { ...local; TimeoutSeconds = 30 }

let remote = {| ...options; Host = "db.example.com" |}

printn $"{remote.Host}:{remote.Port}, timeout {remote.TimeoutSeconds}s"
// db.example.com:5432, timeout 30s

表达式分布接受命名或匿名记录源和目标,包括结构记录。 每个目标字段必须接收一个值。 当多个命名记录类型共享标签时,显式目标批注非常有用。

加强字段类型

后面的字段声明可以替换分布字段的类型。 例如,表示经过分析的 type CustomerId = CustomerId of System.Guid标识符,然后将导入的记录与具有该标识符的记录区分开来:

type ImportedCustomer = { Name: string; Id: string }
type IdentifiedCustomer = { ...ImportedCustomer; Id: CustomerId }

let identify (source: ImportedCustomer) =
    match Guid.TryParse source.Id with
    | true, id -> Ok ({ ...source; Id = CustomerId id }: IdentifiedCustomer)
    | false, _ -> Error "Invalid customer identifier."

Guid.TryParse 验证文本;分布区仅复制字段。 CustomerId 在不实施进一步规则的情况下,区分标识符与其他 GUID。

合并独立模型

从特选名义记录和匿名记录类型撰写声明,然后从独立源组合值:

type PublicProductFields = { Name: string; UnitPrice: decimal }
type ProductRow = { ...PublicProductFields; InternalCost: decimal }
type StockCounts = {| InStock: int; Reserved: int |}

type ProductCard =
    { ...PublicProductFields
      ...StockCounts
      CanOrder: bool }
let product: ProductRow =
    { Name = "Notebook"; UnitPrice = 12.50m; InternalCost = 4m }
let stock: StockCounts = {| InStock = 8; Reserved = 2 |}

let card: ProductCard =
    { ...product; ...stock; CanOrder = stock.InStock > stock.Reserved }

let viewState = {| ...card; Selected = false |}
let selected = {| ...viewState; Selected = true |}

命名源和匿名源提供 ProductCard;然后卡片提供匿名视图状态。 CanOrder 计算一次,以后构造更改计数时,不会重新计算。

Project到已知目标

目标类型选择要保留的字段。 多个源可以提供替换:

let endpoint: Endpoint = { ...options }
let overrides = {| Host = "db.example.com"; Port = 5433 |}
let production: ClientOptions =
    { ...options; ...overrides; TimeoutSeconds = 60 }

printn $"{endpoint.Host}:{endpoint.Port}"
printn $"{production.Host}:{production.Port}, timeout {production.TimeoutSeconds}s"
// localhost:5432
// db.example.com:5433, timeout 60s

endpoint仅Host保留 ;Portproduction合并源和显式重写。 目标中不存在的显式字段仍存在错误。

未批注的匿名记录采用所有参与字段。 命名目标必须可推断。 未知源需要自己的注释,例如 (source: ClientOptions) 函数参数。

字段顺序和重写

贡献内容从左到右读取。 对于重复的字段名称, 最右侧的贡献将获胜。 规则适用于类型中的字段声明和表达式中的字段值。

let laterHost = {| Host = "db.example.com" |}
let corrected: Endpoint = { ...local; ...laterHost; Port = 5433 }

在类型重写后,提供替换类型的值。 重复的显式字段,例如 { Host = "first"; Host = "second" },保留错误。

对于类型声明,获胜字段显示在获胜贡献的位置。 源中的字段保留其源顺序。 此顺序对于构造函数和签名一致性很重要。

评估和复制

传播源表达式按源顺序运行一次,即使以后的贡献将替换它们提供的每个字段:

let visits = ResizeArray<string>()
let hostFrom source host =
    visits.Add source
    {| Host = host |}

let selected =
    {| ...hostFrom "first" "localhost"
       ...hostFrom "second" "db.example.com" |}

assert (selected.Host = "db.example.com")
assert (Seq.toList visits = [ "first"; "second" ])

分布创建一条包含浅字段副本的新记录,以及与显式分配相同的转换。 字段引用的可变对象仍保持共享状态。

嵌套字段更新使用分布,就像复制和更新一样:

let original = {| Connection = local; Retries = 3 |}
let changed = {| ...original; Connection.Port = 5433 |}

assert (changed.Connection.Port = 5433)
assert (original.Connection.Port = 5432)

稍后为替换该字段的早期嵌套更新所做的贡献 Connection 。 将故意替代放在它们应覆盖的分布之后。

有关阴影字段的警告

以后的分布可能会意外撤消显式选择。 编译器默认报告以下警告:

Warning Meaning
FS3897 类型中的字段分散对早期显式字段声明进行阴影。
FS3898 表达式中的字段分散了早期显式字段绑定的阴影。

例如, { Host = "db.example.com"; ...local } 取 Host 自 local,而不是 "db.example.com"。

若要审核其他重叠情况,请启用以下 选择加入警告:

Warning Meaning
FS3905 类型分散从早期类型分布中的字段。
FS3906 显式字段会遮蔽早期分布中的字段。
FS3907 表达式分散了来自早期表达式分布的字段的阴影。
<WarnOn>$(WarnOn);3905;3906;3907</WarnOn>

这些通知不会改变最右侧的行为。

表示形式和 API 兼容性

类型分散复制字段可变性和字段级别属性。 它们不复制类型级属性;目标自己的 [<Struct>] 属性确定其表示形式。 源类型参数必须满足源的泛型约束,包括度量单位约束。

Important

仅当共享演变是有意的时,才使用类型分布。 源字段更改可以更改目标的公共字段、构造函数、属性或约束,而无需编辑该目标。

分散不会绕过辅助功能检查。 可为空记录不是有效的传播源;在使用该值之前,确定可为 null 的值为非 null。 通过分布循环依赖项被拒绝。 普通递归记录字段不只因为声明也使用分布而禁止。

类型分散在 .fsi 文件中工作。 当实现使用分散时,签名可以使用显式字段,或者相反,前提是生成的字段及其顺序达成一致。 引号包含精心策划的记录构造,而不是分布节点。

选择分散或复制和更新

从一个源对同一类型的更新使用 复制和更新表达式 。 使用分布将源、项目合并到其他记录类型或重复使用字段声明。 不要合并 with 在同 ... 一记录表达式中。

分布不适用于集合、对象表达式、任意.NET对象或 C# 记录。 它们不提供记录模式语法。 记录构造函数 是单独的 F# 11.2 功能。

另请参阅