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:
- Zkopírují se jako typy hodnot a mají sémantiku typu hodnoty.
- 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
.
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>]