Diskriminerade fackföreningar
Diskriminerade fackföreningar ger stöd för värden som kan vara ett av flera namngivna fall, eventuellt var och en med olika värden och typer. Diskriminerade fackföreningar är användbara för heterogena data. data som kan ha särskilda fall, inklusive giltiga och felfall. data som varierar i typ från en instans till en annan. och som ett alternativ för små objekthierarkier. Dessutom används rekursiva diskriminerade fackföreningar för att representera träddatastrukturer.
Syntax
[ attributes ]
type [accessibility-modifier] type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
[ member-list ]
Anmärkningar
Diskriminerade fackföreningar liknar unionstyper på andra språk, men det finns skillnader. Precis som med en unionstyp i C++ eller en varianttyp i Visual Basic är data som lagras i värdet inte fasta. Det kan vara ett av flera olika alternativ. Till skillnad från fackföreningar på dessa andra språk ges dock var och en av de möjliga alternativen en ärendeidentifierare. Ärendeidentifierarna är namn på de olika möjliga typer av värden som objekt av den här typen kan vara. värdena är valfria. Om det inte finns några värden motsvarar ärendet ett uppräkningsfall. Om det finns värden kan varje värde antingen vara ett enda värde av en angiven typ eller en tuppel som aggregerar flera fält av samma eller olika typer. Du kan ge ett enskilt fält ett namn, men namnet är valfritt, även om andra fält i samma fall namnges.
Tillgänglighet för diskriminerade fackföreningar är som standard public
.
Tänk dig till exempel följande deklaration av en formtyp.
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
Föregående kod deklarerar en diskriminerad union Form, som kan ha värden för någon av de tre fallen: Rektangel, Cirkel och Prisma. Varje fall har en annan uppsättning fält. Rektangelärendet har två namngivna fält, båda av typen float
, som har namnen bredd och längd. Circle-objektet innehåller bara ett namngivet fält, radie. Prismfallet har tre fält, varav två (bredd och höjd) är namngivna fält. Namnlösa fält kallas anonyma fält.
Du skapar objekt genom att ange värden för de namngivna och anonyma fälten enligt följande exempel.
let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)
Den här koden visar att du antingen kan använda de namngivna fälten i initieringen, eller så kan du förlita dig på ordningen på fälten i deklarationen och bara ange värdena för varje fält i tur och ordning. Konstruktörsanropet för rect
i föregående kod använder de namngivna fälten, men konstruktörsanropet för circ
använder ordningen. Du kan blanda de ordnade fälten och namngivna fält, som i konstruktionen av prism
.
Typen option
är en enkel diskriminerande union i F#-kärnbiblioteket. Den option
typen deklareras enligt följande.
// The option type is a discriminated union.
type Option<'a> =
| Some of 'a
| None
Den tidigare koden anger att typen Option
är en diskriminerad union som har två fall, Some
och None
. Det Some
fallet har ett associerat värde som består av ett anonymt fält vars typ representeras av typparametern 'a
. Det None
fallet har inget associerat värde. Därför anger option
typ en allmän typ som antingen har ett värde av någon typ eller inget värde. Typen Option
har också en alias med små bokstäver, option
, som används oftare.
Ärendeidentifierarna kan användas som konstruktorer för den diskriminerade unionstypen. Följande kod används till exempel för att skapa värden av den option
typen.
let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None
Ärendeidentifierarna används också i mönstermatchningsuttryck. I ett mönstermatchningsuttryck tillhandahålls identifierare för de värden som är associerade med de enskilda fallen. Till exempel, i följande kod har x
identifieraren värdet som är kopplat till fallet Some
av typen option
.
let printValue opt =
match opt with
| Some x -> printfn "%A" x
| None -> printfn "No value."
I mönstermatchningsuttryck kan du använda namngivna fält för att ange diskriminerade unionsmatchningar. För den formtyp som deklarerades tidigare kan du använda de namngivna fälten som följande kod visar för att extrahera fältens värden.
let getShapeWidth shape =
match shape with
| Rectangle(width = w) -> w
| Circle(radius = r) -> 2. * r
| Prism(width = w) -> w
Normalt kan ärendeidentifierarna användas utan att kvalificera dem med namnet på facket. Om du vill att namnet alltid ska vara kvalificerat med namnet på unionen kan du använda attributet RequireQualifiedAccess för union-typdefinitionen.
Avpackning av diskriminerade unioner
I F# Används ofta diskriminerade fackföreningar i domänmodellering för att omsluta en enda typ. Det är enkelt att extrahera det underliggande värdet via mönstermatchning också. Du behöver inte använda ett matchningsuttryck för ett enskilt ärende:
let ([UnionCaseIdentifier] [values]) = [UnionValue]
Följande exempel visar detta:
type ShaderProgram = | ShaderProgram of id:int
let someFunctionUsingShaderProgram shaderProgram =
let (ShaderProgram id) = shaderProgram
// Use the unwrapped value
...
Mönstermatchning tillåts också direkt i funktionsparametrar, så du kan avlägsna ett enskilt fall där.
let someFunctionUsingShaderProgram (ShaderProgram id) =
// Use the unwrapped value
...
Struktur Diskriminerade unioner
Du kan också representera diskriminerade unioner som strukturer. Detta görs med attributet [<Struct>]
.
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of string
| Case2 of int
| Case3 of double
Eftersom det här är värdetyper och inte referenstyper finns det extra överväganden jämfört med referensdiskriminerade fackföreningar:
- De kopieras som värdetyper och har värdetypssemantik.
- Du kan inte använda en rekursiv typdefinition med en multicase struct-diskriminerad union.
Före F# 9 fanns det ett krav på att varje ärende skulle ange ett unikt ärendenamn (inom unionen). Från och med F# 9 hävs begränsningen.
Använda diskriminerade fackföreningar i stället för objekthierarkier
Du kan ofta använda en diskriminerad union som ett enklare alternativ till en liten objekthierarki. Till exempel kan följande diskriminerade union användas i stället för en Shape
basklass som har härledda typer för cirkel, kvadrat och så vidare.
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
I stället för en virtuell metod för att beräkna ett område eller en perimeter, som du skulle använda i en objektorienterad implementering, kan du använda mönstermatchning för att förgrena till lämpliga formler för att beräkna dessa kvantiteter. I följande exempel används olika formler för att beräkna området, beroende på formen.
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)
Utdata är följande:
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
Använda diskriminerade fackföreningar för träddatastrukturer
Diskriminerade fackföreningar kan vara rekursiva, vilket innebär att själva facket kan ingå i typen av ett eller flera fall. Rekursiva diskriminerade fackföreningar kan användas för att skapa trädstrukturer som används för att modellera uttryck i programmeringsspråk. I följande kod används en rekursiv disjunkt union för att skapa en binär trädstruktur. Unionen består av två fall, Node
, som är en nod med ett heltalsvärde och vänster och höger underträd, och Tip
, som avslutar trädet.
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
I föregående kod har resultSumTree
värdet 10. Följande bild visar trädstrukturen för myTree
.
Diskriminerade fackföreningar fungerar bra om noderna i trädet är heterogena. I följande kod representerar typen Expression
det abstrakta syntaxträdet för ett uttryck på ett enkelt programmeringsspråk som stöder addition och multiplikation av tal och variabler. Vissa av unionsfallen är inte rekursiva och representerar antingen tal (Number
) eller variabler (Variable
). Andra fall är rekursiva och representerar åtgärder (Add
och Multiply
), där operanderna också är uttryck. Funktionen Evaluate
använder ett matchningsuttryck för att rekursivt bearbeta syntaxträdet.
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
När den här koden körs är värdet för result
5.
Medlemmar
Det är möjligt att definiera medlemmar i diskriminerade fackföreningar. I följande exempel visas hur du definierar en egenskap och implementerar ett gränssnitt:
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*
egenskaper för ärenden
Sedan F# 9 exponerar diskriminerade unioner automatiskt genererade egenskaper .Is*
för varje fall, vilket gör att du kan kontrollera om ett värde är av en viss typ.
Så här kan den användas:
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
Vanliga attribut
Följande attribut ses ofta i diskriminerade fackföreningar:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]