Unterscheidungs-Unions

Unterscheidungs-Unions bieten Unterstützung für Werte, bei denen es sich um einen Fall aus einer Anzahl verschiedener benannter Fälle handeln kann, mit jeweils potenziell unterschiedlichen Werten und Typen. Unterscheidungs-Unions sind für heterogene Daten nützlich: Daten, die besondere Fälle enthalten können, einschließlich gültiger und fehlerhafter Fälle, Daten, die sich im Typ von einer Instanz zur nächsten unterscheiden und als Alternative für kleine Objekthierarchien. Außerdem werden rekursive Unterscheidungs-Unions verwendet, um Baumstrukturdatenstrukturen darzustellen.

Syntax

[ attributes ]
type [accessibility-modifier] type-name =
    | case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
    | case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]

    [ member-list ]

Bemerkungen

Unterscheidungs-Unions ähneln Union-Typen in anderen Sprachen, aber es gibt Unterschiede. Wie bei einem Union-Typ in C++ oder einem Variantentyp in Visual Basic werden die im Wert gespeicherten Daten nicht korrigiert. Der Typ kann einer von mehreren unterschiedlichen Optionen sein. Im Gegensatz zu Gewerkschaften in diesen anderen Sprachen wird jeder der möglichen Optionen ein Fallbezeichner zugewiesen. Die Fallbezeichner sind Namen für die verschiedenen möglichen Typen von Werten, die Objekte dieses Typs aufweisen können. Die Werte sind optional. Wenn keine Werte vorhanden sind, entspricht der Fall einem Enumerationsfall. Wenn Werte vorhanden sind, kann jeder Wert entweder ein einzelner Wert eines angegebenen Typs oder ein Tupel sein, das mehrere Felder gleicher oder unterschiedlicher Typen aggregiert. Sie können einem einzelnen Feld einen Namen geben, der Name ist jedoch optional, selbst wenn andere Felder im gleichen Fall benannt sind.

Barrierefreiheit für diskriminierte Gewerkschaften sind standardmäßig public.

Betrachten Sie zum Beispiel die folgende Deklaration eines Formtyps:

type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * float * height : float

Der vorangehende Code deklariert eine Unterscheidungs-Union-Form, die Werte der folgenden drei Fälle haben kann: Rechteck, Kreis und Prisma. Jeder Fall hat einen anderen Satz von Feldern. Der Rechteckfall hat zwei benannte Felder, beide vom Typ float mit den Namen Breite und Länge. Der Kreisfall hat nur ein benanntes Feld, nämlich Radius. Der Fall Prism weist drei Felder auf, von denen zwei (Breite und Höhe) benannte Felder sind. Unbenannte Felder werden als anonyme Felder bezeichnet.

Sie erstellen Objekte, indem Sie Werte für die benannten und anonymen Felder bereitstellen, wie in den folgenden Beispielen dargestellt.

let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)

Dieser Code zeigt, dass Sie entweder die benannten Felder in der Initialisierung verwenden können, oder Sie können die Reihenfolge der Felder in der Deklaration erstellen und nur die Werte für jedes Feld bereitstellen. Der Konstruktoraufruf für rect im vorherigen Code verwendet benannte Felder, der Konstruktoraufruf für circ hingegen die Reihenfolge. Sie können die angeordneten Felder und benannten Felder wie in der Konstruktion von prism kombinieren.

Der option-Typ ist eine einfache Unterscheidungs-Union in der F#-Kernbibliothek. Der option-Typ wird folgendermaßen deklariert.

// The option type is a discriminated union.
type Option<'a> =
    | Some of 'a
    | None

Der vorherige Code gibt an, dass der Option-Typ eine Unterscheidungs-Union mit den beiden Fällen Some und None ist. Der Some-Fall verfügt über einen zugeordneten Wert, der aus einem anonymen Feld besteht, dessen Typ durch den Typparameter 'a dargestellt wird. Der None-Fall verfügt über keinen zugeordneten Wert. Der option-Typ gibt also einen generischen Typ an, der entweder einen Wert eines Typs oder keinen Wert aufweist. Der Option-Typ weist außerdem ein kleingeschriebenes Typalias auf, option, das häufig verwendet wird.

Die Fallbezeichner können für den Unterscheidungs-Union-Typ als Konstruktoren verwendet werden. Der folgende Code wird z. B. verwendet, um Werte des option-Typs zu erstellen.

let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None

Die Fallbezeichner werden auch in Musterabgleichsausdrücken verwendet. In einem Musterabgleichsausdruck werden Bezeichner für die Werte bereitgestellt, die den einzelnen Fällen zugeordnet sind. Im folgenden Code ist x z. B. der Bezeichner, dem der Wert des Some-Falls des option-Typs zugeordnet ist.

let printValue opt =
    match opt with
    | Some x -> printfn "%A" x
    | None -> printfn "No value."

in Mustervergleichsausdrücken können Sie benannte Felder verwenden, um Unterscheidungs-Union-Übereinstimmungen anzugeben. Für den zuvor deklarierten Formtyp können Sie die benannten Felder verwenden, um die Werte von Feldern zu extrahieren, wie der folgende Code zeigt.

let getShapeWidth shape =
    match shape with
    | Rectangle(width = w) -> w
    | Circle(radius = r) -> 2. * r
    | Prism(width = w) -> w

Normalerweise können die Fallbezeichner verwendet werden, ohne sie durch den Namen der Union zu qualifizieren. Soll der Name immer mit dem Namen der Union qualifiziert werden, können Sie das RequireQualifiedAccess-Attribut für die Gewerkschafts-Typdefinition übernehmen.

Diskriminierte Gewerkschaften auspacken

In F# werden Diskriminierte Gewerkschaften häufig bei der Domänenmodellierung für den Umbruch eines einzelnen Typs verwendet. Es ist einfach, den zugrunde liegenden Wert auch über den Musterabgleich zu extrahieren. Sie müssen keinen Übereinstimmungsausdruck für einen einzelnen Fall verwenden:

let ([UnionCaseIdentifier] [values]) = [UnionValue]

Dies wird im folgenden Beispiel veranschaulicht:

type ShaderProgram = | ShaderProgram of id:int

let someFunctionUsingShaderProgram shaderProgram =
    let (ShaderProgram id) = shaderProgram
    // Use the unwrapped value
    ...

Musterabgleich ist auch direkt in Funktionsparametern zulässig, sodass Sie dort einen einzelnen Fall entpacken können:

let someFunctionUsingShaderProgram (ShaderProgram id) =
    // Use the unwrapped value
    ...

Diskriminierte Gewerkschaften strukturieren

Sie können diskriminierte Gewerkschaften auch als Struktur darstellen. Verwenden Sie dazu das [<Struct>]-Attribut:

[<Struct>]
type SingleCase = Case of string

[<Struct>]
type Multicase =
    | Case1 of Case1 : string
    | Case2 of Case2 : int
    | Case3 of Case3 : double

Da es sich hierbei um Werttypen und keine Verweistypen handelt, gibt es zusätzliche Überlegungen im Vergleich zu Referenzdiskriminierten Gewerkschaften:

  1. Sie werden als Werttypen kopiert und weisen eine Werttypsemantik auf.
  2. Sie können keine rekursive Typdefinition mit einer Mehrbuchstaben-Struktur Diskriminierte Gewerkschaft verwenden.
  3. Sie müssen eindeutige Groß-/Kleinschreibungsnamen für eine Mehrcase-Struktur Diskriminierte Gewerkschaft angeben.

Verwenden von Unterscheidungs-Unions statt Objekthierarchien

Sie können oft eine Unterscheidungs-Union als einfachere Alternative zu einer kleinen Objekthierarchie verwenden. Die folgende Unterscheidungs-Union könnte z. B. statt einer Shape-Basisklasse verwendet werden, die abgeleitete Typen für Kreis, Quadrat usw. aufweist.

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

Statt einer virtuellen Methode zur Berechnung eines Bereich oder eines Umkreises, wie sie in einer objektorientierten Implementierung verwendet werden würde, können Sie mithilfe des Mustervergleichs die entsprechenden Formeln aufteilen, um die Mengen zu berechnen. Im folgenden Beispiel werden andere Formeln verwendet, um den Bereich abhängig von der Form zu berechnen.

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)

Die Ausgabe lautet wie folgt:

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

Verwenden von Unterscheidungs-Unions für Datenstrukturen

Unterscheidungs-Unions können rekursiv sein, d. h., dass die Union selbst im Typ eines Falles oder mehrerer Fälle enthalten sein kann. Rekursive Unterscheidungs-Unions können verwendet werden, um Strukturen zur Gestaltung von Ausdrücken in Programmiersprachen zu erstellen. Im folgenden Code wird eine rekursive Unterscheidungs-Union verwendet, um eine binäre Strukturdatenstruktur zu erstellen. Die Union besteht aus zwei Fällen: Node, der ein Knoten mit einem ganzzahligen Wert und linken und rechten Unterstrukturen ist, und Tip, der die Struktur beendet.

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

Im vorherigen Code verfügt resultSumTree über den Wert 10. Die folgende Abbildung zeigt die Struktur für myTree an.

Diagram that shows the tree structure for myTree.

Unterscheidungs-Unions funktionieren gut, wenn die Knoten in der Struktur heterogen sind. Im folgenden Code stellt der Expression-Typ die abstrakte Syntaxstruktur eines Ausdrucks in einer einfachen Programmiersprache dar, die Addition und Multiplikation von Zahlen und Variablen unterstützt. Einige der Union-Fälle sind nicht rekursiv und stellen entweder Zahlen (Number) oder Variablen (Variable) dar. Andere Fälle sind rekursiv und stellen Operationen (Add und Multiply) dar, wobei die Operanden auch Ausdrücke sind. Die Evaluate-Funktion verwendet einen Vergleichsausdruck, um die Syntaxstruktur rekursiv zu verarbeiten.

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

Wenn dieser Code ausgeführt wird, beträgt der Wert von result 5.

Member

Es ist möglich, Mitglieder für diskriminierte Gewerkschaften zu definieren. Das folgende Beispiel zeigt, wie eine Eigenschaft definiert und eine Schnittstelle implementiert wird:

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}"

Allgemeine Attribute

Die folgenden Attribute werden häufig in diskriminierten Gewerkschaften gesehen:

  • [<RequireQualifiedAccess>]
  • [<NoEquality>]
  • [<NoComparison>]
  • [<Struct>]

Weitere Informationen