Unie dyskryminowane
Związki dyskryminowane zapewniają wsparcie dla wartości, które mogą być jedną z wielu nazwanych przypadków, prawdopodobnie z różnymi wartościami i typami. Związki dyskryminujące są przydatne w przypadku danych heterogenicznych, danych, które mogą mieć specjalne przypadki, w tym przypadki prawidłowe i przypadki błędów; danych, które różnią się typem od jednego przypadku do drugiego; oraz jako alternatywa dla małych hierarchii obiektów. Ponadto rekursywne związki dyskryminacyjne są używane do reprezentowania struktur danych drzewa.
Składnia
[ attributes ]
type [accessibility-modifier] type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
[ member-list ]
Uwagi
Związki dyskryminowane są podobne do typów unii w innych językach, ale istnieją różnice. Podobnie jak w przypadku typu unii w języku C++ lub typu wariantu w Visual Basic, dane przechowywane w wartości nie są stałe; może to być jedna z kilku różnych opcji. W przeciwieństwie do związków w tych innych językach, każda z możliwych opcji ma jednak identyfikator sprawy. Identyfikatory przypadków to nazwy różnych możliwych typów wartości, które mogą być obiektami tego typu; wartości są opcjonalne. Jeśli wartości nie są obecne, przypadek jest odpowiednikiem przypadku wyliczenia. Jeśli są dostępne wartości, każda z nich może być pojedynczą wartością określonego typu lub krotką, która agreguje wiele pól tego samego lub różnych typów. Możesz nadać nazwę pojedynczemu polu, ale nazwa jest opcjonalna, nawet jeśli inne pola w tym samym przypadku mają nazwę.
Ułatwienia dostępu dla związków zawodowych, które są dyskryminowane, są ustawiane domyślnie na public
.
Rozważmy na przykład następującą deklarację typu Kształt.
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
Powyższy kod deklaruje unię dyskryminacyjną "Shape", która może przyjmować wartości dowolnego z trzech wariantów: prostokąta, koła i pryzmatu. Każdy przypadek ma inny zestaw pól. Przypadek prostokąta ma dwa nazwane pola, oba typu float
, które mają szerokość i długość nazw. Klasa Circle ma tylko jedno nazwane pole, promień. Przypadek Prism ma trzy pola, z których dwa (szerokość i wysokość) są nazwane polami. Pola nienazwane są określane jako pola anonimowe.
Obiekty tworzy się, podając wartości dla nazwanych i anonimowych pól zgodnie z poniższymi przykładami.
let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)
Ten kod pokazuje, że można użyć nazwanych pól w inicjowaniu lub polegać na kolejności pól w deklaracji i po prostu podać wartości dla każdego pola z kolei. Wywołanie konstruktora dla rect
w poprzednim kodzie używa nazwanych pól, ale wywołanie konstruktora dla circ
używa kolejności. Można mieszać uporządkowane pola i nazwane pola, tak jak w budowie prism
.
Typ option
to prosty dyskryminowany związek w podstawowej bibliotece języka F#. Typ option
jest zadeklarowany w następujący sposób.
// The option type is a discriminated union.
type Option<'a> =
| Some of 'a
| None
Poprzedni kod określa, że typ Option
jest związkiem dyskryminowanym, który ma dwa przypadki, Some
i None
. Przypadek Some
ma skojarzona wartość składającą się z jednego pola anonimowego, którego typ jest reprezentowany przez parametr typu 'a
. Przypadek None
nie ma skojarzonej wartości. W związku z tym typ option
określa typ ogólny, który ma albo wartość pewnego typu, albo nie ma wartości. Typ Option
ma również alias zapisany małymi literami, option
, który jest częściej używany.
Identyfikatory przypadków mogą być używane jako konstruktory dla typu unii dyskryminowanej. Na przykład poniższy kod służy do tworzenia wartości typu option
.
let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None
Identyfikatory przypadków są również używane w wyrażeniach dopasowywania wzorców. W wyrażeniu dopasowania wzorca identyfikatory są udostępniane dla wartości skojarzonych z poszczególnymi przypadkami. Na przykład w poniższym kodzie x
jest identyfikatorem przypisaną wartością, która jest związana z przypadkiem Some
typu option
.
let printValue opt =
match opt with
| Some x -> printfn "%A" x
| None -> printfn "No value."
W wyrażeniach dopasowywania wzorców można użyć nazwanych pól, aby określić dopasowania związków dyskryminowanych. W przypadku zadeklarowanego wcześniej typu kształtu możesz użyć nazwanych pól, jak pokazano w poniższym kodzie, aby wyodrębnić wartości pól.
let getShapeWidth shape =
match shape with
| Rectangle(width = w) -> w
| Circle(radius = r) -> 2. * r
| Prism(width = w) -> w
Zwykle identyfikatory przypadków mogą być używane bez podawania nazwy unii. Jeśli chcesz, aby nazwa była zawsze poprzedzana nazwą związku, możesz zastosować atrybut RequireQualifiedAccess do definicji typu związku.
Unwrapping Dyskryminowane związki zawodowe
Unie dyskryminacyjne w języku F# są często używane w modelowaniu dziedziny do obudowania typu. Łatwo jest również wyodrębnić wartość bazową za pomocą dopasowywania wzorca. Nie musisz używać wyrażenia dopasowania dla pojedynczego przypadku:
let ([UnionCaseIdentifier] [values]) = [UnionValue]
W poniższym przykładzie pokazano następujące kwestie:
type ShaderProgram = | ShaderProgram of id:int
let someFunctionUsingShaderProgram shaderProgram =
let (ShaderProgram id) = shaderProgram
// Use the unwrapped value
...
Dopasowywanie wzorca jest również dozwolone bezpośrednio w parametrach funkcji, dzięki czemu można tam odpakować pojedynczy przypadek:
let someFunctionUsingShaderProgram (ShaderProgram id) =
// Use the unwrapped value
...
Rozróżnione unie strukturalne
Można również reprezentować związki dyskryminujące jako struktury. Odbywa się to za pomocą atrybutu [<Struct>]
.
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of string
| Case2 of int
| Case3 of double
Ponieważ są to typy wartości, a nie typy referencyjne, istnieją dodatkowe kwestie w porównaniu z uniami dyskryminowanymi na podstawie typów referencyjnych.
- Są one kopiowane jako typy wartości i mają semantykę typu wartości.
- Nie można użyć rekursywnej definicji typu z unii dyskryminowanej ze strukturą wielowariantową.
Przed numerem F# 9 dla każdego przypadku istniał wymóg określenia unikatowej nazwy sprawy (w unii). Od wersji języka F# 9 ograniczenie zostało zniesione.
Używanie związków dyskryminowanych zamiast hierarchii obiektów
Często można użyć dyskryminowanej unii jako prostszej alternatywy dla małej hierarchii obiektów. Na przykład następująca dyskryminowana unia może być używana zamiast klasy bazowej Shape
, która ma typy pochodne dla okręgu, kwadratu itd.
type Shape =
// The value here is the radius.
| Circle of float
// The value here is the side length.
| EquilateralTriangle of double
// The value here is the side length.
| Square of double
// The values here are the height and width.
| Rectangle of double * double
Zamiast metody wirtualnej do obliczania obszaru lub obwodu, jak w implementacji zorientowanej obiektowo, można użyć dopasowania wzorca do przejścia do odpowiednich formuł do obliczania tych wartości. W poniższym przykładzie różne formuły są używane do obliczania obszaru w zależności od kształtu.
let pi = 3.141592654
let area myShape =
match myShape with
| Circle radius -> pi * radius * radius
| EquilateralTriangle s -> (sqrt 3.0) / 4.0 * s * s
| Square s -> s * s
| Rectangle(h, w) -> h * w
let radius = 15.0
let myCircle = Circle(radius)
printfn "Area of circle that has radius %f: %f" radius (area myCircle)
let squareSide = 10.0
let mySquare = Square(squareSide)
printfn "Area of square that has side %f: %f" squareSide (area mySquare)
let height, width = 5.0, 10.0
let myRectangle = Rectangle(height, width)
printfn "Area of rectangle that has height %f and width %f is %f" height width (area myRectangle)
Dane wyjściowe są następujące:
Area of circle that has radius 15.000000: 706.858347
Area of square that has side 10.000000: 100.000000
Area of rectangle that has height 5.000000 and width 10.000000 is 50.000000
Używanie związków dyskryminowanych dla struktur danych drzewa
Związki dyskryminacyjne mogą być rekursywne, co oznacza, że sam związek może być uwzględniony w typie co najmniej jednego przypadku. Rekursywne związki dyskryminacyjne mogą służyć do tworzenia struktur drzew, które są używane do modelowania wyrażeń w językach programowania. W poniższym kodzie rekursywna unia dyskryminacyjna służy do tworzenia binarnej struktury danych drzewa. Związek składa się z dwóch przypadków, Node
, który jest węzłem z wartością całkowitą i lewym i prawym poddrzewami, oraz Tip
, co oznacza zakończenie drzewa.
type Tree =
| Tip
| Node of int * Tree * Tree
let rec sumTree tree =
match tree with
| Tip -> 0
| Node(value, left, right) -> value + sumTree (left) + sumTree (right)
let myTree =
Node(0, Node(1, Node(2, Tip, Tip), Node(3, Tip, Tip)), Node(4, Tip, Tip))
let resultSumTree = sumTree myTree
W poprzednim kodzie resultSumTree
ma wartość 10. Poniższa ilustracja przedstawia strukturę drzewa dla myTree
.
Związki dyskryminujące działają dobrze, jeśli węzły w drzewie są heterogeniczne. W poniższym kodzie typ Expression
reprezentuje abstrakcyjne drzewo składni wyrażenia w prostym języku programowania, który obsługuje dodawanie i mnożenie liczb i zmiennych. Niektóre przypadki unii nie są rekursywne i reprezentują liczby (Number
) lub zmienne (Variable
). Inne przypadki to rekursywne i reprezentują operacje (Add
i Multiply
), gdzie operandy są również wyrażeniami. Funkcja Evaluate
używa wyrażenia dopasowania do cyklicznego przetwarzania drzewa składni.
type Expression =
| Number of int
| Add of Expression * Expression
| Multiply of Expression * Expression
| Variable of string
let rec Evaluate (env: Map<string, int>) exp =
match exp with
| Number n -> n
| Add(x, y) -> Evaluate env x + Evaluate env y
| Multiply(x, y) -> Evaluate env x * Evaluate env y
| Variable id -> env[id]
let environment = Map [ "a", 1; "b", 2; "c", 3 ]
// Create an expression tree that represents
// the expression: a + 2 * b.
let expressionTree1 = Add(Variable "a", Multiply(Number 2, Variable "b"))
// Evaluate the expression a + 2 * b, given the
// table of values for the variables.
let result = Evaluate environment expressionTree1
Po wykonaniu tego kodu wartość result
wynosi 5.
Członkowie
Możliwe jest zdefiniowanie członków związków dyskryminowanych. W poniższym przykładzie pokazano, jak zdefiniować właściwość i zaimplementować interfejs:
open System
type IPrintable =
abstract Print: unit -> unit
type Shape =
| Circle of float
| EquilateralTriangle of float
| Square of float
| Rectangle of float * float
member this.Area =
match this with
| Circle r -> Math.PI * (r ** 2.0)
| EquilateralTriangle s -> s * s * sqrt 3.0 / 4.0
| Square s -> s * s
| Rectangle(l, w) -> l * w
interface IPrintable with
member this.Print () =
match this with
| Circle r -> printfn $"Circle with radius %f{r}"
| EquilateralTriangle s -> printfn $"Equilateral Triangle of side %f{s}"
| Square s -> printfn $"Square with side %f{s}"
| Rectangle(l, w) -> printfn $"Rectangle with length %f{l} and width %f{w}"
.Is*
właściwości w odniesieniu do przypadków
Od wersji F# 9 unie dyskryminujące udostępniają automatycznie generowane .Is*
właściwości dla każdego przypadku, co pozwala sprawdzić, czy wartość należy do określonego przypadku.
W ten sposób można go używać:
type Contact =
| Email of address: string
| Phone of countryCode: int * number: string
type Person = { name: string; contact: Contact }
let canSendEmailTo person =
person.contact.IsEmail // .IsEmail is auto-generated
Typowe atrybuty
Następujące atrybuty są często spotykane w związkach dyskryminowanych:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]