Sdílet prostřednictvím


Diskriminovaná unie

Diskriminovaná unie poskytuje podporu pro hodnoty, které mohou být jedním z několika pojmenovaných případů, přičemž každý může mít různé hodnoty a typy. Diskriminované sjednocení jsou užitečné pro heterogenní data; údaje, které mohou mít zvláštní případy, včetně platných a chybových případů; data, která se liší podle typu od jedné instance po jinou; a jako alternativu pro malé hierarchie objektů. Rekurzivní diskriminované sjednocení se navíc používají k reprezentaci stromových datových struktur.

Syntax

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

    [ member-list ]

Poznámky

Diskriminované sjednocení jsou podobné typům sjednocení v jiných jazycích, ale existují rozdíly. Stejně jako u sjednocovacího typu v jazyce C++ nebo variantního typu v jazyce Visual Basic nejsou data uložená v hodnotě pevná; může to být jedna z několika různých možností. Na rozdíl od svazů v těchto jiných jazycích má však každá z možných variant identifikátor případu. Identifikátory případu jsou názvy různých možných typů hodnot, které mohou být objekty tohoto typu; hodnoty jsou volitelné. Pokud hodnoty nejsou k dispozici, případ odpovídá případu výčtu. Pokud existují hodnoty, může být každá hodnota buď jedna hodnota zadaného typu, nebo řazená kolekce členů, která agreguje více polí stejného nebo různých typů. Jednotlivé pole můžete pojmenovat, ale název je nepovinný, i když jsou pojmenována jiná pole ve stejném případě.

Přístupnost pro diskriminační unie má výchozí hodnotu: public.

Představte si například následující deklaraci typu obrazce.

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

Předchozí kód deklaruje diskriminovaný sjednocovací obrazec, který může mít hodnoty ze tří případů: Rectangle, Circle a Prism. Každý případ má jinou sadu polí. Obdélníkový případ má dvě pojmenovaná pole, obě typu float, které mají názvy šířky a délky. Případ kruhu má pouze jedno pojmenované pole, poloměr. Případ Prism má tři pole, z nichž dvě (šířka a výška) jsou pojmenovaná pole. Nepojmenovaná pole se označují jako anonymní pole.

Objekty vytvoříte zadáním hodnot pro pojmenovaná a anonymní pole podle následujících příkladů.

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

Tento kód ukazuje, že v inicializaci můžete buď použít pojmenovaná pole, nebo se můžete spolehnout na řazení polí v deklaraci a zadat hodnoty pro jednotlivá pole zase. Volání konstruktoru pro rect v předchozím kódu používá pojmenovaná pole, ale volání konstruktoru pro circ používá řazení. Uspořádaná pole a pojmenovaná pole můžete kombinovat stejně jako při vytváření prism.

Typ option je jednoduchá diskriminovaná unie v hlavní knihovně F#. Typ option je deklarován následujícím způsobem.

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

Předchozí kód určuje, že typ Option je diskriminovaná unie, která má dva případy, Some a None. Případ Some má přidruženou hodnotu, která se skládá z jednoho anonymního pole, jehož typ je reprezentován parametrem typu 'a. Případ None nemá přidruženou hodnotu. Typ option tedy určuje obecný typ, který má buď hodnotu určitého typu, nebo žádnou hodnotu. Typ Option má také alias psaný malými písmeny, option, který je běžněji používán.

Identifikátory případů lze použít jako konstruktory pro diskriminovaný typ sjednocení. Například následující kód slouží k vytvoření hodnot typu option.

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

Identifikátory případů se také používají ve vzorových shodných výrazech. Ve výrazu pro porovnání vzorů jsou identifikátory přiřazené hodnotám přidruženým k jednotlivým případům. Například v následujícím kódu je x identifikátor přiřazený hodnotě, která je spojena s případem Some typu option.

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

Ve výrazech pro porovnávání vzorů můžete pomocí pojmenovaných polí určit diskriminovaná unie. Pro typ obrazce, který byl deklarován dříve, můžete použít pojmenovaná pole k extrahování hodnot polí, jak ukazuje následující kód.

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

Za normálních okolností lze případové identifikátory použít, aniž by byly kvalifikovány názvem sdružení. Pokud chcete, aby bylo jméno vždy kvalifikováno s názvem unie, můžete použít atribut RequireQualifiedAccess v definici typu unie.

Rozbalení Diskriminovaných Unií

Ve F# se diskriminované unie často používají při modelování domény pro obalení jednoho typu. Základní hodnotu můžete snadno extrahovat také pomocí porovnávání vzorů. Pro jediný případ nemusíte používat výraz shody:

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

Následující příklad ukazuje toto:

type ShaderProgram = | ShaderProgram of id:int

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

Porovnávání vzorů je také povolené přímo v parametrech funkce, takže můžete vybalit jeden případ:

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

Diskriminované unie struktur

Můžete také představit diskriminované unie jako struktury. To se provádí s atributem [<Struct>].

[<Struct>]
type SingleCase = Case of string

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

Vzhledem k tomu, že se jedná o typy hodnot, nikoli odkazové typy, existují další aspekty v porovnání s odkazovými diskriminovanými sjednoceními:

  1. Zkopírují se jako typy hodnot a mají sémantiku typu hodnoty.
  2. Nelze použít definici rekurzivního typu s diskriminovanou unií struktury s více případy.

Před F# 9 bylo pro každý případ nutné zadat jedinečný název případu (v rámci unie). Počínaje F# 9 je omezení zrušeno.

Použití diskriminovaných sjednocení místo hierarchií objektů

Diskriminovanou unii můžete často použít jako jednodušší alternativu k menší hierarchii objektů. Například následující diskriminovaná unie by se mohla použít místo základní třídy Shape, která má odvozené typy pro kruh, čtverec atd.

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

Místo virtuální metody pro výpočet plochy nebo obvodu, jak byste použili v objektově orientované implementaci, můžete použít porovnávání vzorů k větvení na příslušné vzorce pro výpočet těchto hodnot. V následujícím příkladu se k výpočtu oblasti v závislosti na obrazci používají různé vzorce.

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)

Výstup je následující:

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

Použití rozlišovaných sjednocení pro stromové datové struktury

Diskriminované sjednocení mohou být rekurzivní, což znamená, že samotná unie může být zahrnuta do typu jednoho nebo více případů. Rekurzivní diskriminované sjednocení lze použít k vytváření stromových struktur, které se používají k modelování výrazů v programovacích jazycích. V následujícím kódu se rekurzivní diskriminovaná sjednocení používá k vytvoření datové struktury binárního stromu. Sjednocení se skládá ze dvou případů, Node, což je uzel s celočíselnou hodnotou a s levým a pravým podstromem, a Tip, který ukončuje strom.

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

V předchozím kódu má resultSumTree hodnotu 10. Následující obrázek znázorňuje strukturu stromu pro myTree.

Diagram znázorňující strukturu stromu pro myTree

Diskriminované sjednocení funguje dobře, pokud jsou uzly ve stromě heterogenní. V následujícím kódu představuje typ Expression abstraktní syntaxi výrazu v jednoduchém programovacím jazyce, který podporuje sčítání a násobení čísel a proměnných. Některé případy sjednocení nejsou rekurzivní a představují čísla (Number) nebo proměnné (Variable). Jiné případy jsou rekurzivní a představují operace (Add a Multiply), kde operandy jsou také výrazy. Funkce Evaluate používá výraz shody k rekurzivnímu zpracování stromu syntaxe.

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

Při spuštění tohoto kódu je hodnota result 5.

Vzájemně rekurzivní diskriminované unie

Diskriminovaná sjednocení v F# mohou být vzájemně rekurzivní, což znamená, že na sebe mohou rekurzivně odkazovat různé typy sjednocení. To je užitečné při modelování hierarchických nebo vzájemně propojených struktur. K definování vzájemně rekurzivních diskriminovaných sjednocení použijte klíčové slovo and.

Představte si například reprezentaci abstraktního stromu syntaxe (AST), kde výrazy můžou obsahovat příkazy a příkazy můžou obsahovat výrazy:

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

Členové

Je možné definovat členy v diskriminovaných uniích. Následující příklad ukazuje, jak definovat vlastnost a implementovat rozhraní:

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* vlastnosti případů

Od verze F# 9 diskriminované unie odhalují automaticky generované vlastnosti .Is* pro jednotlivé případy, což vám umožňuje zkontrolovat, zda hodnota patří k určitému případu.

Můžete ho použít takto:

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

Běžné atributy

V diskriminovaných sjednoceních se běžně používají následující atributy:

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

Viz také