Sumy rozłączne
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; dane, które mogą mieć specjalne przypadki, w tym prawidłowe i błędy; dane, które różnią się w typie od jednego wystąpienia do drugiego; i alternatywą 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, jednak każda z możliwych opcji ma identyfikator przypadku. 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 wartości są obecne, każda wartość 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 dyskryminowanych związków zawodowych są domyślnie ustawione 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 dyskryminowany kształt unii, który może mieć wartości dowolnego z trzech przypadków: prostokąt, okrąg i pryzmat. 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. Przypadek 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 w rect
poprzednim kodzie używa nazwanych pól, ale wywołanie konstruktora używa circ
kolejności. Można mieszać uporządkowane pola i nazwane pola, tak jak w przypadku budowy prism
elementu .
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ść, która składa się z jednego pola anonimowego, którego typ jest reprezentowany przez parametr 'a
typu . Sprawa None
nie ma skojarzonej wartości. W związku z tym option
typ określa typ ogólny, który ma wartość typu lub nie ma wartości. Typ Option
ma również małe litery aliasu , 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 option
typu.
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, który jest skojarzony z Some
przypadkiem option
typu.
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 kwalifikowania ich z nazwą unii. Jeśli chcesz, aby nazwa zawsze została kwalifikowana z nazwą unii, możesz zastosować atrybut RequireQualifiedAccess do definicji typu unii.
Unwrapping Dyskryminowane związki zawodowe
W unii dyskryminowanej języka F# są często używane w modelowaniu domeny do opakowywania pojedynczego 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
...
Związki dyskryminujące struktury
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 Case1 : string
| Case2 of Case2 : int
| Case3 of Case3 : double
Ponieważ są to typy wartości, a nie typy referencyjne, istnieją dodatkowe zagadnienia w porównaniu z związkami dyskryminowanym odwołaniami:
- Są one kopiowane jako typy wartości i mają semantyka typu wartości.
- Nie można użyć cyklicznej definicji typu z wielocasową strukturą dyskryminowaną Unii.
- Należy podać unikatowe nazwy przypadków dla wielocasowej struktury Dyskryminowana Unia.
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, tak jak w implementacji zorientowanej na obiekt, można użyć dopasowania wzorca do rozgałęzienia do odpowiednich formuł do obliczenia tych iloś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)
Wynik jest następujący:
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
czyli węzła z wartością całkowitą i lewą i prawą poddrzewami oraz Tip
, co kończy drzewo.
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 elementu 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 są rekursywne i reprezentują operacje (Add
i Multiply
), gdzie operandy są również wyrażeniami. Funkcja Evaluate
używa wyrażenia dopasowania, aby rekursywnie przetwarzać drzewo 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
to 5.
Elementy członkowskie
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}"
Typowe atrybuty
Następujące atrybuty są często spotykane w związkach dyskryminowanych:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]