Registros (F#)

Registros representam agregações simples de valores nomeados, opcionalmente com membros. Eles podem ser estruturas ou tipos de referência. Eles são tipos de referência por padrão.

Sintaxe

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

Comentários

Na sintaxe anterior, typename é o nome do tipo de registro, label1 e label2 são nomes de valores, conhecidos como labels, e type1 e type2 são os tipos desses valores. member-list é a lista opcional de membros do tipo. É possível usar o atributo [<Struct>] para criar um registro de struct em vez de um registro que seja um tipo de referência.

Estes são alguns exemplos:

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

Quando cada rótulo está em uma linha separada, o ponto e vírgula é opcional.

É possível definir valores em expressões conhecidas como expressões de registro. O compilador infere o tipo dos rótulos usados (se eles forem suficientemente distintos daqueles de outros tipos de registro). As chaves ({ }) delimitam a expressão de registro. O código a seguir mostra uma expressão de registro que inicializa um registro com três elementos flutuantes com rótulos x, y e z.

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

Não use a forma abreviada se houver outro tipo com os mesmos rótulos.

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 }

Os rótulos do tipo declarado mais recentemente têm precedência sobre os do tipo declarado anteriormente, portanto, mypoint3D é inferido como Point3D no exemplo anterior. É possível especificar explicitamente o tipo de registro, como no código a seguir.

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

Métodos podem ser definidos para tipos de registro e tipos de classe.

Criar registros usando expressões de registro

É possível inicializar registros usando os rótulos definidos neles. A expressão de registro faz isso. Use chaves para delimitar a expressão de registro e o ponto e vírgula como delimitador.

O exemplo a seguir mostra como criar um registro.

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

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

Os pontos e vírgulas após o último campo na expressão de registro e na definição de tipo são opcionais, independentemente de os campos estarem todos em uma linha.

Ao criar um registro, é necessário fornecer valores para cada campo. Não é possível fazer referência aos valores de outros campos na expressão de inicialização de um campo.

No código a seguir, o tipo de myRecord2 é inferido dos nomes dos campos. Opcionalmente, é possível especificar o nome do tipo explicitamente.

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

Outra forma de construção de registro pode ser útil quando você precisa copiar um registro existente e, possivelmente, alterar alguns dos valores de campo. A linha de código a seguir ilustra esse caso.

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

Esta forma da expressão de registro é chamada de expressão de registro de cópia e atualização.

Os registros são imutáveis por padrão. No entanto, é possível criar registros modificados com facilidade usando uma expressão de cópia e atualização. Também é possível especificar explicitamente um campo mutável.

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

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

myCar.Odometer <- myCar.Odometer + 21

Não use o atributo DefaultValue com campos de registro. Uma abordagem melhor é definir instâncias padrão de registros com campos que são inicializados com valores padrão e, em seguida, usar uma expressão de registro de cópia e atualização para definir quaisquer campos que sejam diferentes dos valores padrão.

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

Criar registros mutuamente recursivos

Às vezes, ao criar um registro, você pode querer que ele dependa de outro tipo que você definirá posteriormente. Isso é um erro de compilação, a menos os tipos de registro sejam mutuamente recursivos.

A definição de registros mutuamente recursivos é feita com a palavra-chave and. Isso permite vincular dois ou mais tipos de registro.

Por exemplo, o seguinte código define os tipos Person e Address como mutuamente recursivos:

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

Para criar instâncias de ambos, faça o seguinte:

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

Ao definir o exemplo anterior sem a palavra-chave and, ele não é compilado. A palavra-chave and é necessária para definições mutuamente recursivas.

Correspondência de padrões com registros

Os registros podem ser usados com a correspondência de padrões. É possível especificar alguns campos explicitamente e fornecer variáveis para outros que serão atribuídos no caso de uma correspondência. O exemplo de código a seguir ilustra isso.

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 }

Veja a seguir a saída desse código.

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

Registros e membros

É possível especificar membros em registros da mesma forma que em classes. Não há suporte para campos. Uma abordagem comum é definir um membro estático Default para facilitar a construção do registro:

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

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

let defaultPerson = Person.Default

Ao usar um identificador próprio, esse identificador se refere à instância do registro cujo membro é chamado:

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()

Diferenças entre registros e classes

Os campos de registro diferem dos campos de classe porque são expostos automaticamente como propriedades e são usados na criação e na cópia de registros. A construção de registros também difere da construção de classes. Em um tipo de registro, não é possível definir um construtor. Em vez disso, aplica-se a sintaxe de construção descrita neste tópico. As classes não têm relação direta entre os parâmetros, os campos e as propriedades do construtor.

Assim como os tipos de união e estrutura, os registros têm semântica de igualdade estrutural. As classes têm semântica de igualdade de referência. O código de exemplo a seguir demonstra isso.

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."

A saída deste código é a seguinte:

The records are equal.

Ao escrever o mesmo código com classes, os dois objetos de classe seriam desiguais porque os dois valores representariam dois objetos no heap e somente os endereços seriam comparados (a menos que o tipo de classe substitua o método System.Object.Equals).

Se você precisar de igualdade de referência para registros, adicione o atributo [<ReferenceEquality>] acima do registro.

Confira também