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

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 'atypu . 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 , optionktó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:

  1. Są one kopiowane jako typy wartości i mają semantyka typu wartości.
  2. Nie można użyć cyklicznej definicji typu z wielocasową strukturą dyskryminowaną Unii.
  3. 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, Nodeczyli 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.

Diagram that shows the tree structure for 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>]

Zobacz też