Operadores de spread de recordes

Para partilhar campos sem fundir tipos de modelos distintos, utilize spreads de registo, disponíveis a partir de F# 11.

Usar ...Type para contribuir com declarações de campo para um novo registo nomeado, ou ...expression para contribuir com valores de campo para um registo nomeado ou anónimo. A reutilização de declarações e o mapeamento de valores são escolhas independentes.

Compor tipos de registo

Para partilhar campos de ligação entre vários tipos de configuração, espalhe Endpoint numa nova declaração de registo:

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

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

ClientOptions tem Host, Port, e TimeoutSeconds. É um tipo nominal distinto, não um Endpoint subtipo.

Múltiplas fontes e declarações explícitas podem aparecer na mesma definição tipográfica:

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

Apenas os campos de registo participam. Métodos, propriedades que não são campos de registo e membros estáticos não são copiados. Uma fonte pode ser um registo nomeado ou um tipo de registo anónimo, incluindo uma abreviatura de registo anónimo. O destino de um spread tipo deve ser um tipo de registo nomeado.

Compor valores de registo

Copie o endpoint para as opções do cliente e depois use um registo anónimo para substituir o host:

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

Os spreads de expressões aceitam fontes e destinos de registos nomeados ou anónimos, incluindo registos de estruturas. Cada campo de destino deve receber um valor. Uma anotação alvo explícita é útil quando vários tipos de registo nomeados partilham rótulos.

Fortalecer o tipo de um campo

Uma declaração posterior de campo pode substituir o tipo de campo espalhado. Por exemplo, represente um identificador analisado com type CustomerId = CustomerId of System.Guid, e depois distinga um registo importado de um com esse identificador:

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 valida o texto; a dobra apenas copia os campos. CustomerId distingue identificadores de outros GUIDs sem impor regras adicionais.

Modelos independentes combinados

Compõe declarações a partir de um registo nominal curado e de um tipo de registo anónimo, depois reúne valores de fontes independentes:

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 |}

Fontes nomeadas e anónimas fornecem ProductCard; o cartão então fornece uma visão anónima do estado. CanOrder é calculado uma vez, não é recalculado quando uma construção posterior altera as contagens.

Project num alvo conhecido

Um tipo de alvo seleciona os campos a manter. Múltiplas fontes podem fornecer substituições:

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 mantém apenas Host e Port; production combina fontes e sobreposições explícitas. Campos explícitos ausentes do alvo continuam a ser erros.

Um registo anónimo não anotado assume todos os campos contribuídos. Um alvo nomeado deve ser inferível. Fontes desconhecidas precisam das suas próprias anotações, como (source: ClientOptions) num parâmetro de função.

Ordem de campo e sobreposições

As contribuições são lidas da esquerda para a direita. Para um nome de campo repetido, a contribuição mais à direita vence. A regra aplica-se tanto a declarações de campos em tipos como a valores de campo em expressões.

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

Após uma substituição de tipo, forneça um valor do tipo de substituição. Campos explícitos repetidos, como { Host = "first"; Host = "second" }, continuam a ser erros.

Para declarações de tipos, um campo vencedor aparece na posição da sua contribuição vencedora. Os campos de uma fonte mantêm a sua ordem de fonte. Esta ordem é importante para os construtores e para a conformidade com a assinatura.

Avaliação e cópia

Expressões de fonte espalhadas executam-se uma vez cada, por ordem de fonte, mesmo que uma contribuição posterior substitua todos os campos fornecidos:

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" ])

As páginas criam um novo registo com cópias de campo superficiais e as mesmas conversões que as atribuições explícitas. Objetos mutáveis referenciados por campos continuam partilhados.

As atualizações de campo aninhadas funcionam com spreads, tal como acontece com copy-and-update:

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

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

Uma contribuição posterior substitui Connection uma atualização aninhada anterior desse campo. Coloca sobreposições deliberadas após os spreads que devem anular.

Avisos sobre campos sombreados

Uma tiragem posterior pode desfazer acidentalmente uma escolha explícita. O compilador reporta estes avisos por defeito:

Warning Meaning
FS3897 Um campo de uma dispersão de tipo sombreia uma declaração explícita de campo anterior.
FS3898 Um campo de uma expressão espalhada sombreia uma ligação explícita de campo anterior.

Por exemplo, { Host = "db.example.com"; ...local } toma Host de local, não "db.example.com".

Para uma auditoria de outras sobreposições, ative estes avisos de opt-in:

Warning Meaning
FS3905 Um spread de tipo sombreia um campo de um spread de tipo anterior.
FS3906 Um campo explícito sombreia um campo de uma tiragem anterior.
FS3907 Uma expressão espalhada sombreia um campo de uma expressão anterior espalhada.
<WarnOn>$(WarnOn);3905;3906;3907</WarnOn>

Estes avisos não alteram o comportamento de quem ganha à direita.

Representação e compatibilidade com APIs

Espalhamentos de tipo copiam, mutabilidade de campo e atributos ao nível de campo. Não copiam atributos ao nível do tipo; o próprio [<Struct>] atributo do destino determina a sua representação. Os argumentos do tipo fonte devem satisfazer as restrições genéricas da fonte, incluindo as restrições das unidades de medida.

Importante

Use os spreads de tipos apenas quando a evolução partilhada for intencional. Uma alteração no campo de origem pode alterar os campos públicos do destino, o construtor, os atributos ou as restrições sem uma edição nesse destino.

Os spreads não contornam as verificações de acessibilidade. Registos anuláveis não são fontes de dispersão válidas; estabeleça que um valor anulável não é nulo antes de o usar. Dependências cíclicas através de spreads são rejeitadas. Campos de registo recursivo comuns não são proibidos apenas porque a declaração também utiliza uma dispersão.

As tiradas de tipos funcionam em .fsi ficheiros. Uma assinatura pode usar campos explícitos enquanto a implementação usa spreads, ou o inverso, desde que os campos resultantes e a sua ordem concordem. As citações contêm a construção elaborada do registo, não um nó espalhado.

Escolha tiradas ou copiar e atualizar

Use expressões de copiar e atualizar para uma atualização do mesmo tipo proveniente de uma fonte. Use spreads para combinar fontes, projetar para um tipo de registo diferente ou reutilizar declarações de campo. Não combinem with a mesma ... expressão de registo.

Os spreads não se aplicam a coleções, expressões de objetos, objetos .NET arbitrários ou registos C#. Eles não fornecem sintaxe de padrões de registo. Os construtores de registos são uma funcionalidade separada do F# 11.2.

Ver também