Teilen über


Diskriminierte Gewerkschaften

Diskriminierte Gewerkschaften unterstützen Werte, die einer der verschiedenen benannten Fälle sein können, möglicherweise mit unterschiedlichen Werten und Typen. Diskriminierte Gewerkschaften sind für heterogene Daten nützlich; Daten, die spezielle Fälle haben können, einschließlich gültiger und Fehlerfälle; Daten, die von einer Instanz zu einer anderen unterschiedlich sind; und als Alternative für kleine Objekthierarchien. Darüber hinaus werden rekursive diskriminierte Verbände verwendet, um Baumstrukturen 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

Diskriminierte Gewerkschaften ähneln gewerkschaftstypen in anderen Sprachen, aber es gibt Unterschiede. Wie bei einem Union-Typ in C++ oder einem Variant-Typ in Visual Basic sind die im Wert gespeicherten Daten nicht festgelegt; es kann sich um eine von mehreren verschiedenen Optionen handeln. Im Gegensatz zu Gewerkschaften in diesen anderen Sprachen erhält jedoch jede der möglichen Optionen eine Fall-ID. Die Fallbezeichner sind Namen für die verschiedenen möglichen Typen von Werten, die Objekte dieses Typs haben 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 desselben oder verschiedener Typen aggregiert. Sie können einem einzelnen Feld einen Namen geben, aber der Name ist optional, auch wenn andere Felder im selben Fall benannt sind.

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

Betrachten Sie beispielsweise die folgende Deklaration eines Shape-Typs.

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 weist einen anderen Satz von Feldern auf. Das Rechteck-Objekt hat zwei benannte Felder, die beide vom Typ floatsind, 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 gemäß den folgenden Beispielen angeben.

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 sich auf die Reihenfolge der Felder in der Deklaration verlassen und nur die Werte für jedes Feld wiederum angeben. Der Konstruktoraufruf für rect im vorherigen Code verwendet die benannten Felder, aber der Konstruktoraufruf für circ verwendet die Sortierung. 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 wie folgt deklariert.

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

Der vorherige Code gibt an, dass der Typ Option eine diskriminierte Vereinigung mit zwei Fällen, Some und Noneist. Der fall Some hat einen zugeordneten Wert, der aus einem anonymen Feld besteht, dessen Typ durch den Typparameter 'adargestellt wird. Der fall None hat keinen zugeordneten Wert. Daher gibt der option Typ einen generischen Typ an, der entweder einen Wert eines Typs oder keinen Wert aufweist. Der Typ Option verfügt auch über einen kleingeschriebenen Alias, option, der häufiger verwendet wird.

Die Fallbezeichner können als Konstruktoren für den diskriminierten Vereinigungstyp verwendet werden. Der folgende Code wird beispielsweise 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 Mustererkennungsausdrücken verwendet. In einem Musterabgleichsausdruck werden Bezeichner für die Werte bereitgestellt, die den einzelnen Fällen zugeordnet sind. Beispielsweise ist im folgenden Code x der Bezeichner, dem der Wert zugeordnet ist, der mit dem Fall Some des Typs option verbunden 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 Shape-Typ können Sie die benannten Felder wie der folgende Code verwenden, um die Werte der Felder zu extrahieren.

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

Normalerweise können die Fall-IDs verwendet werden, ohne sie mit dem Namen der Vereinigung zu qualifizieren. Wenn der Name immer mit dem Namen der Union qualifiziert werden soll, können Sie das attribut RequireQualifiedAccess auf die Union-Typdefinition anwenden.

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]

Im folgenden Beispiel wird dies 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 Vereinigungen auch als Struktur darstellen. Dies erfolgt mit dem attribut [<Struct>].

[<Struct>]
type SingleCase = Case of string

[<Struct>]
type Multicase =
    | Case1 of string
    | Case2 of int
    | Case3 of 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 rekursive Typdefinitionen nicht mit einer diskriminierten Union mit mehreren Fällen verwenden.

Vor F# 9 gab es für jeden Fall eine Anforderung, einen eindeutigen Fallnamen (innerhalb der Union) anzugeben. Ab F# 9 wird die Einschränkung aufgehoben.

Verwenden diskriminierter Vereinigungen anstelle von Objekthierarchien

Häufig können Sie eine diskriminierte Vereinigung als einfachere Alternative zu einer kleinen Objekthierarchie verwenden. Beispielsweise könnte die folgende diskriminierte Vereinigung anstelle einer Shape Basisklasse verwendet werden, die abgeleitete Typen für Kreis, Quadrat usw. enthält.

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

Anstatt einer virtuellen Methode zur Berechnung einer Fläche oder eines Umfangs, wie Sie es in einer objektorientierten Implementierung verwenden würden, können Sie Musterabgleich nutzen, um zu den entsprechenden Formeln zur Berechnung dieser Größen zu gelangen. Im folgenden Beispiel werden abhängig von der Form unterschiedliche Formeln zum Berechnen des Bereichs verwendet.

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

Das Verwenden von diskriminierten Verbünden für Baumdatenstrukturen

Diskriminierte Gewerkschaften können rekursiv sein, was bedeutet, dass die Gewerkschaft selbst in den Typ eines oder mehrerer Fälle einbezogen werden kann. Rekursive diskriminierte Vereinigungen können verwendet werden, um Baumstrukturen zu erstellen, die verwendet werden, um Ausdrücke in Programmiersprachen zu modellieren. Im folgenden Code wird eine rekursive Unterscheidungs-Union verwendet, um eine binäre Strukturdatenstruktur zu erstellen. Die Verknüpfung besteht aus zwei Fällen: Node, das ein Knoten mit einem ganzzahligen Wert und linken und rechten Unterbäumen ist, und Tip, die den Baum 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 weist resultSumTree den Wert 10 auf. Die folgende Abbildung zeigt die Struktur für myTree an.

Diagramm, das die Struktur für myTree zeigt.

Diskriminierte Gewerkschaften funktionieren gut, wenn die Knoten in der Struktur heterogen sind. Im folgenden Code stellt der Typ Expression die abstrakte Syntaxstruktur eines Ausdrucks in einer einfachen Programmiersprache dar, die das Hinzufügen 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 Vorgänge dar (Add und Multiply), wobei die Operanden auch Ausdrücke sind. Die Evaluate-Funktion verwendet einen Übereinstimmungsausdruck, 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, ist der Wert von result 5.

Wechselseitig rekursive diskriminierte Unions

Diskriminierte Unions in F# können wechselseitig rekursiv sein. Das bedeutet, dass mehrere Union-Typen auf rekursive Weise aufeinander verweisen können. Dies ist hilfreich beim Modellieren hierarchischer oder miteinander verbundener Strukturen. Um gegenseitig rekursive diskriminierte Vereinigungen zu definieren, verwenden Sie das Schlüsselwort and.

Betrachten Sie beispielsweise eine abstrakte Syntaxstruktur (AST)-Darstellung, in der Ausdrücke Anweisungen enthalten können, und Anweisungen können Ausdrücke enthalten:

type Expression =
    | Literal of int
    | Variable of string
    | Operation of string * Expression * Expression
and Statement =
    | Assign of string * Expression
    | Sequence of Statement list
    | IfElse of Expression * Statement * Statement

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

.Is* Eigenschaften bei Fällen

Seit F# 9 machen diskriminierte Vereinigungen automatisch generierte .Is* Eigenschaften für jeden Fall verfügbar, sodass Sie überprüfen können, ob ein Wert von einem bestimmten Fall ist.

So kann es verwendet werden:

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

Allgemeine Attribute

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

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

Siehe auch