若要在不合并不同的模型类型的情况下共享字段,请使用记录分布,从 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 功能。