Operadores de propagação de registro

Para compartilhar campos sem mesclar tipos de modelo distintos, use spreads de registro, disponíveis a partir do F# 11.

Use ...Type para contribuir com declarações de campo para um novo registro nomeado ou ...expression para contribuir com valores de campo para um registro nomeado ou anônimo. Reutilização de declaração e mapeamento de valor são opções independentes.

Compor tipos de registro

Para compartilhar campos de conexão entre tipos de configuração, espalhe Endpoint em uma nova declaração de registro:

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

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

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

Várias fontes e declarações explícitas podem aparecer na mesma definição de tipo:

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

Somente campos de registro participam. Métodos, propriedades que não são campos de registro e membros estáticos não são copiados. Uma origem pode ser um registro nomeado ou um tipo de registro anônimo, incluindo uma abreviação para um registro anônimo. O destino de um spread de tipo deve ser um tipo de registro nomeado.

Redigir valores de registro

Copie o ponto de extremidade para as opções do cliente e use um registro 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ão aceitam fontes de registro e destinos nomeados ou anônimos, incluindo registros struct. Cada campo de destino deve receber um valor. Uma anotação de destino explícita é útil quando vários tipos de registro nomeados compartilham rótulos.

Fortalecer o tipo de um campo

Uma declaração de campo posterior pode substituir o tipo de um campo de distribuição. Por exemplo, represente um identificador analisado e type CustomerId = CustomerId of System.Guid, em seguida, distingue um registro 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; o spread apenas copia campos. CustomerId distingue identificadores de outros GUIDs sem impor regras adicionais.

Combinar modelos independentes

Redigir declarações de um registro nominal coletado e um tipo de registro anônimo e, em seguida, reunir 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; em seguida, o cartão fornece o estado de exibição anônimo. CanOrder é computado uma vez, não recalculado quando uma construção posterior altera as contagens.

Project em um destino conhecido

Um tipo de destino seleciona os campos a serem mantidos. Várias 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 substituições explícitas. Os campos explícitos ausentes do destino permanecem erros.

Um registro anônimo não anotado usa todos os campos de contribuição. Um destino nomeado deve ser inferível. Fontes desconhecidas precisam de suas próprias anotações, como (source: ClientOptions) em um parâmetro de função.

Ordem e substituições de campo

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 se aplica tanto a declarações de campo em tipos quanto 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" }, permanecem erros.

Para declarações de tipo, um campo vencedor aparece na posição de sua contribuição vencedora. Os campos de uma fonte mantêm sua ordem de origem. Essa ordem é importante para construtores e conformidade de assinatura.

Avaliação e cópia

As expressões de origem de propagação são executadas uma vez cada, na ordem de origem, mesmo que uma contribuição posterior substitua cada campo fornecido:

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

Os spreads criam um novo registro com cópias de campo superficial e as mesmas conversões que as atribuições explícitas. Objetos mutáveis referenciados por campos permanecem compartilhados.

As atualizações de campo aninhadas funcionam com spreads, como fazem com a cópia e atualização:

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 para Connection substituir uma atualização aninhada anterior para esse campo. Coloque substituições deliberadas após os spreads que eles devem substituir.

Avisos sobre campos sombreados

Uma propagação posterior pode desfazer acidentalmente uma escolha explícita. O compilador relata esses avisos por padrão:

Warning Meaning
FS3897 Um campo de um tipo espalha sombras uma declaração de campo explícita anterior.
FS3898 Um campo de uma expressão espalha sombras de uma associação de campo explícita anterior.

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

Para uma auditoria de outras sobreposições, habilite estes avisos de aceitação:

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

Esses avisos não alteram o comportamento de rightmost-wins.

Representação e compatibilidade de API

O tipo espalha a mutabilidade do campo de cópia e os atributos de nível de campo. Eles não copiam atributos de nível de tipo; o próprio [<Struct>] atributo do destino determina sua representação. Os argumentos de tipo de origem devem atender às restrições genéricas da origem, incluindo restrições de unidades de medida.

Importante

Use spreads de tipo somente quando a evolução compartilhada for intencional. Uma alteração de campo de origem pode alterar os campos públicos, o construtor, os atributos ou as restrições do destino sem uma edição para esse destino.

Os spreads não ignoram as verificações de acessibilidade. Registros anuláveis não são fontes de propagação válidas; estabelecer que um valor anulável não é nulo antes de usá-lo. As dependências cíclicas por meio de spreads são rejeitadas . Campos de registro recursivo comuns não são proibidos apenas porque a declaração também usa um spread.

Os spreads de tipo funcionam em .fsi arquivos. Uma assinatura pode usar campos explícitos enquanto a implementação usa spreads ou o inverso, desde que os campos resultantes e sua ordem concordem. As aspas contêm a construção de registro elaborada, não um nó de propagação.

Escolher spreads ou copiar e atualizar

Use expressões de cópia e atualização para uma atualização do mesmo tipo de uma fonte. Use spreads para combinar fontes, projetar para um tipo de registro diferente ou reutilizar declarações de campo. Não combine with e ... na mesma expressão de registro.

Os spreads não se aplicam a coleções, expressões de objeto, objetos .NET arbitrários ou registros C#. Eles não fornecem sintaxe de padrão de registro. Construtores de registro são um recurso F# 11.2 separado.

Consulte também