Udostępnij za pośrednictwem


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.

  1. Są one kopiowane jako typy wartości i mają semantykę typu wartości.
  2. 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.

Diagram przedstawiający strukturę drzewa 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>]

Zobacz też