Opérateurs de propagation d’enregistrements

Pour partager des champs sans fusionner des types de modèles distincts, utilisez des répartitions d’enregistrements, disponibles à partir de F# 11.

Permet ...Type de contribuer aux déclarations de champ à un nouvel enregistrement nommé ou ...expression de contribuer à des valeurs de champ à un enregistrement nommé ou anonyme. La réutilisation de la déclaration et le mappage de valeurs sont des choix indépendants.

Composer des types d’enregistrements

Pour partager des champs de connexion entre les types de configuration, répartis Endpoint dans une nouvelle déclaration d’enregistrement :

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

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

ClientOptions has Host, Port, et TimeoutSeconds. Il s’agit d’un type nominal distinct, et non d’un Endpoint sous-type.

Plusieurs sources et déclarations explicites peuvent apparaître dans la même définition de type :

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

Seuls les champs d’enregistrement participent. Les méthodes, les propriétés qui ne sont pas des champs d’enregistrement et les membres statiques ne sont pas copiées. Une source peut être un enregistrement nommé ou un type d’enregistrement anonyme, y compris une abréviation d’un enregistrement anonyme. La destination d’une propagation de type doit être un type d’enregistrement nommé.

Composer des valeurs d’enregistrement

Copiez le point de terminaison dans les options du client, puis utilisez un enregistrement anonyme pour remplacer l’hôte :

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

L’expression répartit accepte les sources et destinations d’enregistrements nommés ou anonymes, y compris les enregistrements de struct. Chaque champ de destination doit recevoir une valeur. Une annotation cible explicite est utile lorsque plusieurs types d’enregistrements nommés partagent des étiquettes.

Renforcer le type d’un champ

Une déclaration de champ ultérieure peut remplacer le type d’un champ de propagation. Par exemple, représentez un identificateur analysé avec type CustomerId = CustomerId of System.Guid, puis distinguez un enregistrement importé d’un avec cet identificateur :

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 valide le texte ; la propagation copie uniquement les champs. CustomerId distingue les identificateurs d’autres GUID sans imposer de règles supplémentaires.

Combiner des modèles indépendants

Composez des déclarations à partir d’un enregistrement nominal organisé et d’un type d’enregistrement anonyme, puis assemblez des valeurs à partir de sources indépendantes :

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

Sources nommées et anonymes fournissent ProductCard; la carte fournit ensuite l’état d’affichage anonyme. CanOrder est calculé une fois, et non recalculé lorsqu’une construction ultérieure modifie le nombre.

Project dans une cible connue

Un type cible sélectionne les champs à conserver. Plusieurs sources peuvent fournir des remplacements :

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 conserve uniquement Host et Port; production combine les sources et les remplacements explicites. Les champs explicites absents de la cible restent des erreurs.

Un enregistrement anonyme non annoté accepte tous les champs fournis. Une cible nommée doit être déduite. Les sources inconnues ont besoin de leurs propres annotations, comme (source: ClientOptions) sur un paramètre de fonction.

Ordre des champs et remplacements

Les contributions sont lues de gauche à droite. Pour un nom de champ répété, la contribution la plus à droite gagne. La règle s’applique à la fois aux déclarations de champ dans les types et aux valeurs de champ dans les expressions.

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

Après un remplacement de type, fournissez une valeur du type de remplacement. Les champs explicites répétés, tels que { Host = "first"; Host = "second" }, restent des erreurs.

Pour les déclarations de type, un champ gagnant apparaît à la position de sa contribution gagnante. Les champs d’une source conservent leur ordre source. Cet ordre est important pour les constructeurs et la conformité des signatures.

Évaluation et copie

Les expressions sources réparties s’exécutent une fois chacune, dans l’ordre source, même si une contribution ultérieure remplace chaque champ qu’ils fournissent :

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

Les répartitions créent un enregistrement avec des copies de champs peu profondes et les mêmes conversions que les affectations explicites. Les objets mutables référencés par les champs restent partagés.

Les mises à jour de champs imbriquées fonctionnent avec des propagations, comme avec la copie et la mise à jour :

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

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

Une contribution ultérieure pour Connection remplacer une mise à jour imbriquée antérieure vers ce champ. Placez des remplacements délibérés après les écarts qu’ils doivent remplacer.

Avertissements relatifs aux champs ombrés

Une propagation ultérieure peut annuler accidentellement un choix explicite. Le compilateur signale ces avertissements par défaut :

Warning Meaning
FS3897 Un champ d’un type s’ombre sur une déclaration de champ explicite antérieure.
FS3898 Un champ d’une expression répartit les ombres d’une liaison de champ explicite antérieure.

Par exemple, { Host = "db.example.com"; ...local } prend Host de local, pas "db.example.com".

Pour un audit d’autres chevauchements, activez ces avertissements d’opt-in :

Warning Meaning
FS3905 Un type répartit un champ à partir d’une propagation de type antérieur.
FS3906 Un champ explicite ombre un champ à partir d’une propagation antérieure.
FS3907 Une expression fait l’ombre à un champ d’une expression antérieure.
<WarnOn>$(WarnOn);3905;3906;3907</WarnOn>

Ces avis ne changent pas le comportement le plus à droite.

Compatibilité de représentation et d’API

Le type répartit la mutabilité des champs de copie et les attributs au niveau du champ. Ils ne copient pas les attributs au niveau du type ; l’attribut de [<Struct>] la destination détermine sa représentation. Les arguments de type source doivent satisfaire les contraintes génériques de la source, y compris les contraintes d’unités de mesure.

Important

Utilisez des répartitions de type uniquement lorsque l’évolution partagée est intentionnelle. Une modification de champ source peut modifier les champs publics de la destination, le constructeur, les attributs ou les contraintes sans modification de cette destination.

Les écarts ne contournent pas les vérifications d’accessibilité. Les enregistrements nullables ne sont pas des sources de propagation valides ; établissez qu’une valeur nullable n’est pas null avant de l’utiliser. Les dépendances cycliques par les propagations sont rejetées . Les champs d’enregistrement récursifs ordinaires ne sont pas interdits simplement parce que la déclaration utilise également une propagation.

Le type répartit le travail dans .fsi les fichiers. Une signature peut utiliser des champs explicites tandis que l’implémentation utilise des propagations, ou l’inverse, à condition que les champs résultants et leur ordre conviennent. Les guillemets contiennent la construction de l’enregistrement élaboré, et non un nœud réparti.

Choisir des répartitions ou copier-mettre à jour

Utilisez des expressions de copie et de mise à jour pour une mise à jour de même type à partir d’une source. Utilisez des répartitions pour combiner des sources, projeter vers un type d’enregistrement différent ou réutiliser des déclarations de champ. Ne combinez with pas et ... dans la même expression d’enregistrement.

Les propagations ne s'appliquent pas aux collections, aux expressions d'objet, aux objets arbitraires .NET ou aux enregistrements C#. Ils ne fournissent pas de syntaxe de modèle d’enregistrement. Les constructeurs d’enregistrements sont une fonctionnalité F# 11.2 distincte.

Voir aussi