Enregistrements (F#)

Les enregistrements représentent des agrégats simples de valeurs nommées, éventuellement avec des membres. Il peut s’agir de structs ou de types références. Il s’agit de types références par défaut.

Syntaxe

[ attributes ]
type [accessibility-modifier] typename =
    { [ mutable ] label1 : type1;
      [ mutable ] label2 : type2;
      ... }
    [ member-list ]

Notes

Dans la syntaxe précédente, typename est le nom du type d’enregistrement, label1 et label2 sont des noms de valeurs, appelées étiquettes, et type1 et type2 sont les types de ces valeurs. member-list est la liste facultative des membres pour le type. Vous pouvez utiliser l’attribut [<Struct>] pour créer un enregistrement struct plutôt qu’un enregistrement qui est un type référence.

Voici quelques exemples.

// Labels are separated by semicolons when defined on the same line.
type Point = { X: float; Y: float; Z: float }

// You can define labels on their own line with or without a semicolon.
type Customer =
    { First: string
      Last: string
      SSN: uint32
      AccountNumber: uint32 }

// A struct record.
[<Struct>]
type StructPoint = { X: float; Y: float; Z: float }

Lorsque chaque étiquette se trouve sur une ligne distincte, le point-virgule est facultatif.

Vous pouvez définir des valeurs dans des expressions appelées expressions d’enregistrement. Le compilateur déduit le type des étiquettes utilisées (si les étiquettes sont suffisamment distinctes de celles des autres types d’enregistrements). Les accolades ({ }) entourent l’expression d’enregistrement. Le code suivant montre une expression d’enregistrement qui initialise un enregistrement à l’aide de trois éléments flottants avec des étiquettes x, y et z.

let mypoint = { X = 1.0; Y = 1.0; Z = -1.0 }

N’utilisez pas le formulaire abrégé si un autre type peut également comporter les mêmes étiquettes.

type Point = { X: float; Y: float; Z: float }
type Point3D = { X: float; Y: float; Z: float }
// Ambiguity: Point or Point3D?
let mypoint3D = { X = 1.0; Y = 1.0; Z = 0.0 }

Les étiquettes du type déclaré le plus récemment prévalent sur celles du type précédemment déclaré. Par conséquent, dans l’exemple précédent, mypoint3D est déduit comme étant Point3D. Vous pouvez spécifier explicitement le type d’enregistrement, comme dans le code suivant.

let myPoint1 = { Point.X = 1.0; Y = 1.0; Z = 0.0 }

Les méthodes peuvent être définies pour les types d’enregistrements comme pour les types de classes.

Création d’enregistrements à l’aide d’expressions d’enregistrement

Vous pouvez initialiser des enregistrements à l’aide des étiquettes définies dans l’enregistrement. Une expression qui effectue cette opération est appelée expression d’enregistrement. Utilisez des accolades pour entourer l’expression d’enregistrement et utilisez le point-virgule en tant que séparateur.

L’exemple suivant montre comment créer un enregistrement.

type MyRecord = { X: int; Y: int; Z: int }

let myRecord1 = { X = 1; Y = 2; Z = 3 }

Les points-virgules après le dernier champ dans l’expression d’enregistrement et dans la définition de type sont facultatifs, que les champs soient tous dans une seule ligne ou non.

Lorsque vous créez un enregistrement, vous devez spécifier des valeurs pour chaque champ. Vous ne pouvez pas faire référence aux valeurs d’autres champs dans l’expression d’initialisation pour un champ quelconque.

Dans le code suivant, le type de myRecord2 est déduit des noms des champs. Vous pouvez éventuellement spécifier le nom de type explicitement.

let myRecord2 =
    { MyRecord.X = 1
      MyRecord.Y = 2
      MyRecord.Z = 3 }

Une autre forme de construction d’enregistrement peut être utile lorsque vous devez copier un enregistrement existant et éventuellement modifier certaines des valeurs de champ. La ligne de code suivante le montre.

let myRecord3 = { myRecord2 with Y = 100; Z = 2 }

Cette forme de l’expression d’enregistrement est appelée l’expression d’enregistrement de copie et de mise à jour.

Les enregistrements sont immuables par défaut. Toutefois, vous pouvez facilement créer des enregistrements modifiés à l’aide d’une expression de copie et de mise à jour. Vous pouvez également spécifier un champ mutable explicitement.

type Car =
    { Make: string
      Model: string
      mutable Odometer: int }

let myCar =
    { Make = "Fabrikam"
      Model = "Coupe"
      Odometer = 108112 }

myCar.Odometer <- myCar.Odometer + 21

N’utilisez pas l’attribut DefaultValue avec les champs d’enregistrement. Une meilleure approche consiste à définir des instances par défaut d’enregistrements avec des champs qui sont initialisés en valeurs par défaut, puis à utiliser une expression d’enregistrement de copie et de mise à jour pour définir tous les champs qui diffèrent des valeurs par défaut.

// Rather than use [<DefaultValue>], define a default record.
type MyRecord =
    { Field1 : int
      Field2 : int }

let defaultRecord1 = { Field1 = 0; Field2 = 0 }
let defaultRecord2 = { Field1 = 1; Field2 = 25 }

// Use the with keyword to populate only a few chosen fields
// and leave the rest with default values.
let rr3 = { defaultRecord1 with Field2 = 42 }

Création d’enregistrements mutuellement récursifs

Lors de la création d’un enregistrement, vous pouvez vouloir qu’il dépende d’un autre type à définir ultérieurement. Il s’agit d’une erreur de compilation, sauf si vous définissez les types d’enregistrements comme étant mutuellement récursifs.

La définition d’enregistrements mutuellement récursifs est effectuée à l’aide du mot clé and. Cela vous permet de lier 2 types d’enregistrements ou plus.

Par exemple, le code suivant définit des types Person et Address comme étant mutuellement récursifs :

// Create a Person type and use the Address type that is not defined
type Person =
  { Name: string
    Age: int
    Address: Address }
// Define the Address type which is used in the Person record
and Address =
  { Line1: string
    Line2: string
    PostCode: string
    Occupant: Person }

Pour créer des instances des deux types, procédez comme suit :

// Create a Person type and use the Address type that is not defined
let rec person =
  {
      Name = "Person name"
      Age = 12
      Address =
          {
              Line1 = "line 1"
              Line2 = "line 2"
              PostCode = "abc123"
              Occupant = person
          }
  }

Si vous deviez définir l’exemple précédent sans le mot clé and, il n’y aurait pas de compilation. Le mot clé and est requis pour les définitions mutuellement récursives.

Critères spéciaux avec des enregistrements

Les enregistrements peuvent être utilisés avec des critères spéciaux. Vous pouvez spécifier certains champs explicitement et spécifier des variables pour d’autres champs qui seront attribués en cas de correspondance. L'exemple de code suivant illustre ceci.

type Point3D = { X: float; Y: float; Z: float }
let evaluatePoint (point: Point3D) =
    match point with
    | { X = 0.0; Y = 0.0; Z = 0.0 } -> printfn "Point is at the origin."
    | { X = xVal; Y = 0.0; Z = 0.0 } -> printfn "Point is on the x-axis. Value is %f." xVal
    | { X = 0.0; Y = yVal; Z = 0.0 } -> printfn "Point is on the y-axis. Value is %f." yVal
    | { X = 0.0; Y = 0.0; Z = zVal } -> printfn "Point is on the z-axis. Value is %f." zVal
    | { X = xVal; Y = yVal; Z = zVal } -> printfn "Point is at (%f, %f, %f)." xVal yVal zVal

evaluatePoint { X = 0.0; Y = 0.0; Z = 0.0 }
evaluatePoint { X = 100.0; Y = 0.0; Z = 0.0 }
evaluatePoint { X = 10.0; Y = 0.0; Z = -1.0 }

La sortie de ce code est la suivante.

Point is at the origin.
Point is on the x-axis. Value is 100.000000.
Point is at (10.000000, 0.000000, -1.000000).

Enregistrements et membres

Vous pouvez spécifier des membres sur des enregistrements, à l’instar des classes. Les champs ne sont pas pris en charge. Une approche courante consiste à définir un membre statique Default pour faciliter la construction d’enregistrements :

type Person =
  { Name: string
    Age: int
    Address: string }

    static member Default =
        { Name = "Phillip"
          Age = 12
          Address = "123 happy fun street" }

let defaultPerson = Person.Default

Si vous utilisez un auto-identificateur, cet identificateur fait référence à l’instance de l’enregistrement dont le membre est appelé :

type Person =
  { Name: string
    Age: int
    Address: string }

    member this.WeirdToString() =
        this.Name + this.Address + string this.Age

let p = { Name = "a"; Age = 12; Address = "abc123" }
let weirdString = p.WeirdToString()

Différences entre les enregistrements et les classes

Les champs d’enregistrement diffèrent des champs de classe en ce qu’ils sont automatiquement exposés en tant que propriétés et utilisés dans la création et la copie d’enregistrements. La construction d’enregistrements diffère également de la construction de classes. Dans un type d’enregistrement, vous ne pouvez pas définir un constructeur. Au lieu de cela, la syntaxe de construction décrite dans cette rubrique s’applique. Les classes n’ont aucune relation directe entre les paramètres du constructeur, les champs et les propriétés.

Comme les types d’union et de structure, les enregistrements présentent une sémantique d’égalité structurelle. Les classes présentent une sémantique d’égalité de référence. L'exemple de code suivant illustre cette tâche.

type RecordTest = { X: int; Y: int }

let record1 = { X = 1; Y = 2 }
let record2 = { X = 1; Y = 2 }

if (record1 = record2) then
    printfn "The records are equal."
else
    printfn "The records are unequal."

La sortie de ce code est la suivante :

The records are equal.

Si vous écrivez le même code avec des classes, les deux objets de classe seraient inégaux, car les deux valeurs représenteraient deux objets sur le tas et seules les adresses seraient comparées (sauf si le type de classe écrase la méthode System.Object.Equals).

Si vous avez besoin d’une égalité de référence pour les enregistrements, ajoutez l’attribut [<ReferenceEquality>] au-dessus de l’enregistrement.

Voir aussi