Rekordy (F#)
Rekordy reprezentują proste agregacje nazwanych wartości, opcjonalnie z elementami członkowskimi. Mogą one być strukturami lub typami referencyjnymi. Domyślnie są to typy referencyjne.
Składnia
[ attributes ]
type [accessibility-modifier] typename =
{ [ mutable ] label1 : type1;
[ mutable ] label2 : type2;
... }
[ member-list ]
Uwagi
W poprzedniej składni typename jest nazwą typu rekordu, label1 i label2 są nazwami wartości, określanymi jako etykiety, a type1 i type2 są typami tych wartości. Lista elementów członkowskich jest opcjonalną listą elementów członkowskich dla typu. Za pomocą atrybutu [<Struct>]
można utworzyć rekord struktury, a nie rekord, który jest typem odwołania.
Poniżej przedstawiono kilka przykładów.
// 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 }
Gdy każda etykieta znajduje się w osobnym wierszu, średnik jest opcjonalny.
Wartości można ustawić w wyrażeniach nazywanych wyrażeniami rekordów. Kompilator wywnioskuje typ z używanych etykiet (jeśli etykiety są wystarczająco różne od tych z innych typów rekordów). Nawiasy klamrowe ({ }) otaczają wyrażenie rekordu. Poniższy kod przedstawia wyrażenie rekordu, które inicjuje rekord z trzema elementami zmiennoprzecinkowymi z etykietami x
i y
z
.
let mypoint = { X = 1.0; Y = 1.0; Z = -1.0 }
Nie używaj skróconego formularza, jeśli może istnieć inny typ, który ma również te same etykiety.
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 }
Etykiety ostatnio zadeklarowanego typu mają pierwszeństwo przed etykietami wcześniej zadeklarowanego typu, więc w poprzednim przykładzie mypoint3D
wywnioskowany Point3D
jest . Możesz jawnie określić typ rekordu, tak jak w poniższym kodzie.
let myPoint1 = { Point.X = 1.0; Y = 1.0; Z = 0.0 }
Metody można zdefiniować dla typów rekordów tak samo jak dla typów klas.
Tworzenie rekordów przy użyciu wyrażeń rekordów
Rekordy można zainicjować przy użyciu etykiet zdefiniowanych w rekordzie. Wyrażenie, które to robi, jest nazywane wyrażeniem rekordu. Użyj nawiasów klamrowych, aby ująć wyrażenie rekordu i użyć średnika jako ogranicznika.
W poniższym przykładzie pokazano, jak utworzyć rekord.
type MyRecord = { X: int; Y: int; Z: int }
let myRecord1 = { X = 1; Y = 2; Z = 3 }
Średniki po ostatnim polu w wyrażeniu rekordu i w definicji typu są opcjonalne, niezależnie od tego, czy pola znajdują się w jednym wierszu.
Podczas tworzenia rekordu należy podać wartości dla każdego pola. Nie można odwoływać się do wartości innych pól w wyrażeniu inicjowania dla dowolnego pola.
W poniższym kodzie typ myRecord2
jest wnioskowany z nazw pól. Opcjonalnie możesz jawnie określić nazwę typu.
let myRecord2 =
{ MyRecord.X = 1
MyRecord.Y = 2
MyRecord.Z = 3 }
Inna forma konstruowania rekordów może być przydatna, gdy trzeba skopiować istniejący rekord i ewentualnie zmienić niektóre wartości pól. Poniższy wiersz kodu ilustruje to.
let myRecord3 = { myRecord2 with Y = 100; Z = 2 }
Ta forma wyrażenia rekordu jest nazywana wyrażeniem kopiowania i aktualizowania rekordu.
Rekordy są domyślnie niezmienne; można jednak łatwo tworzyć zmodyfikowane rekordy przy użyciu wyrażenia kopiowania i aktualizowania. Można również jawnie określić pole modyfikowalne.
type Car =
{ Make: string
Model: string
mutable Odometer: int }
let myCar =
{ Make = "Fabrikam"
Model = "Coupe"
Odometer = 108112 }
myCar.Odometer <- myCar.Odometer + 21
Nie używaj atrybutu DefaultValue z polami rekordu. Lepszym rozwiązaniem jest zdefiniowanie domyślnych wystąpień rekordów z polami, które są inicjowane do wartości domyślnych, a następnie używanie wyrażenia kopiowania i aktualizowania rekordu w celu ustawienia wszystkich pól, które różnią się od wartości domyślnych.
// 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 }
Tworzenie wzajemnie cyklicznych rekordów
Czasami podczas tworzenia rekordu może być konieczne posiadanie go w zależności od innego typu, który chcesz zdefiniować później. Jest to błąd kompilacji, chyba że zdefiniujesz typy rekordów, które mają być wzajemnie rekursywne.
Definiowanie wzajemnie cyklicznych rekordów odbywa się za pomocą słowa kluczowego and
. Umożliwia to łączenie 2 lub większej liczby typów rekordów.
Na przykład następujący kod definiuje typ Person
i Address
jako wzajemnie cykliczny:
// 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 }
Aby utworzyć wystąpienia obu tych elementów, wykonaj następujące czynności:
// 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
}
}
Jeśli chcesz zdefiniować poprzedni przykład bez słowa kluczowego and
, nie zostanie skompilowany. Słowo and
kluczowe jest wymagane dla wzajemnie rekursywnych definicji.
Dopasowywanie wzorca z rekordami
Rekordy mogą być używane z dopasowaniem wzorca. Możesz jawnie określić niektóre pola i podać zmienne dla innych pól, które będą przypisywane po wystąpieniu dopasowania. Pokazano to w poniższym przykładzie kodu.
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 }
Dane wyjściowe tego kodu są następujące.
Point is at the origin.
Point is on the x-axis. Value is 100.000000.
Point is at (10.000000, 0.000000, -1.000000).
Rekordy i elementy członkowskie
Możesz określić elementy członkowskie na rekordach, podobnie jak w przypadku klas. Brak obsługi pól. Typowym podejściem jest zdefiniowanie statycznego elementu członkowskiego w celu łatwej Default
konstrukcji rekordu:
type Person =
{ Name: string
Age: int
Address: string }
static member Default =
{ Name = "Phillip"
Age = 12
Address = "123 happy fun street" }
let defaultPerson = Person.Default
Jeśli używasz identyfikatora własnego, ten identyfikator odwołuje się do wystąpienia rekordu, którego element członkowski jest wywoływany:
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()
Różnice między rekordami i klasami
Pola rekordów różnią się od pól klas, które są automatycznie udostępniane jako właściwości, i są używane podczas tworzenia i kopiowania rekordów. Rekordowa konstrukcja różni się również od konstrukcji klasy. W typie rekordu nie można zdefiniować konstruktora. Zamiast tego ma zastosowanie składnia konstrukcji opisana w tym temacie. Klasy nie mają bezpośredniej relacji między parametrami konstruktora, polami i właściwościami.
Podobnie jak typy unii i struktury, rekordy mają semantyka równości strukturalnej. Klasy mają semantyka równości odwołań. W poniższym przykładzie kodu pokazano to.
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."
Dane wyjściowe tego kodu są następujące:
The records are equal.
Jeśli napiszesz ten sam kod z klasami, dwa obiekty klas byłyby nierówne, ponieważ dwie wartości reprezentują dwa obiekty na stercie, a tylko adresy zostaną porównane (chyba że typ klasy zastąpi metodę System.Object.Equals
).
Jeśli potrzebujesz równości odwołań dla rekordów, dodaj atrybut [<ReferenceEquality>]
powyżej rekordu.