事件
歧視聯集
判別聯集為可以是多個具名案例之一的值提供支援,可能每個案例都有不同的值和類型。 判別聯合對於異質數據非常有用;這類數據可以包含特殊情況,包括有效和錯誤的案例;類型隨實例不同而變化的數據;以及作為小型物件階層的替代方案。 此外,遞迴判別聯集可用來表示樹形資料結構。
[ attributes ]
type [accessibility-modifier] type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
[ member-list ]
判別聯合與其他語言中的聯合類型類似,但有所不同。 如同在 C++ 中的等位型別或 Visual Basic 中的變體類型,儲存在值中的數據不會固定;它可以是數個不同的選項之一。 不過,不同於其他語言的聯合,每個可能的選項都會有一個 案例標識符。 案例標識碼是此類型物件可能之各種可能值類型的名稱;這些值是選擇性的。 如果值不存在,則案例相當於列舉案例。 如果存在值,每個值可以是指定類型的單一值,也可以是聚合相同或不同類型的多個欄位的元組。 您可以指定個別欄位的名稱,但名稱是選擇性的,即使相同案例中的其他欄位命名也一樣。
具區分標記的聯合體的可訪問性其預設值為 public
。
例如,請考慮下列 Shape 類型的宣告。
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
上述程式碼會宣告一個區分聯集 Shape,其值可以是三個案例中的任一個:Rectangle、Circle 和 Prism。 每個案例都有一組不同的欄位。 Rectangle有兩個具名欄位,兩個類型為 float
,名稱分別為寬度和長度。 Circle 案例只有一個具名欄位,半徑。 Prism 案例有三個字段,其中兩個字段(寬度和高度)是命名字段。 未命名的欄位稱為匿名欄位。
您可以根據下列範例,提供具名和匿名欄位的值來建構物件。
let rect = Rectangle(length = 1.3, width = 10.0)
let circ = Circle (1.0)
let prism = Prism(5., 2.0, height = 3.0)
此程式代碼顯示您可以在初始化中使用具名字段,也可以依賴宣告中的欄位順序,並只針對每個欄位提供值。 上一個程式代碼中 rect
的建構函式呼叫會使用具名字段,但 circ
的建構函式呼叫會使用排序。 您可以混合使用有序欄位和具名欄位,就像在建構 prism
時那樣。
option
類型是 F# 核心程式庫中的簡單區別聯合。
option
類型宣告如下。
// The option type is a discriminated union.
type Option<'a> =
| Some of 'a
| None
上一個程式代碼指定類型 Option
是一個包含兩個情況的區分聯合,分別是 Some
和 None
。
Some
案例具有相關聯的值,其中包含一個匿名欄位,其類型是由類型參數 'a
表示。
None
案例沒有相關聯的值。 因此,option
型別會指定具有某些型別值或無值的泛型型別。 類型 Option
也有小寫類型別名,option
,較常用。
案例識別碼可以作為區分聯合型別的建構函式使用。 例如,下列程式代碼可用來建立 option
類型的值。
let myOption1 = Some(10.0)
let myOption2 = Some("string")
let myOption3 = None
案例標識碼也會用於模式比對表達式中。 在模式比對表達式中,會為與個別案例相關聯的值提供標識碼。 例如,在下列程式代碼中,x
是標識符,指定與 option
類型 Some
案例相關聯的值。
let printValue opt =
match opt with
| Some x -> printfn "%A" x
| None -> printfn "No value."
在模式比對表達式中,您可以使用具名欄位來指定區分聯集的比對。 針對先前宣告的 Shape 類型,您可以使用具名欄位,如下列程式代碼所示,來擷取欄位的值。
let getShapeWidth shape =
match shape with
| Rectangle(width = w) -> w
| Circle(radius = r) -> 2. * r
| Prism(width = w) -> w
通常,案例識別碼可以直接使用,不需要附帶聯合體的名稱。 如果您希望名稱總是添加聯合名稱限定,您可以將 RequireQualifiedAccess 屬性套用至聯合類型定義。
在 F# 中,歧視聯集常用於領域建模,包裝單一類型。 也很容易透過模式比對來擷取基礎值。 您不需要針對單一案例使用比對表示式:
let ([UnionCaseIdentifier] [values]) = [UnionValue]
下列範例示範了這一點:
type ShaderProgram = | ShaderProgram of id:int
let someFunctionUsingShaderProgram shaderProgram =
let (ShaderProgram id) = shaderProgram
// Use the unwrapped value
...
函式參數中也直接允許模式匹配,因此您可以在其中處理個別案例:
let someFunctionUsingShaderProgram (ShaderProgram id) =
// Use the unwrapped value
...
您也可以將可辨識聯合體表示為結構。 這會使用 [<Struct>]
屬性來完成。
[<Struct>]
type SingleCase = Case of string
[<Struct>]
type Multicase =
| Case1 of string
| Case2 of int
| Case3 of double
因為這些是實值型別,而不是參考型別,因此相較於參考區分聯集,需要額外的考量:
- 它們會複製為實值型別,並具有實值型別語意。
- 您無法使用遞歸類型定義搭配多寫結構區分等位。
在 F# 9 之前,在聯合體中每個個案都必須指定一個唯一的個案名稱。 從 F# 9 開始,會解除限制。
您通常可以使用歧視聯集做為較簡單的小型物件階層替代方案。 例如,可以使用下列區分聯集,而不是具有圓形、正方形等衍生型別的 Shape
基類。
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
與其像在面向對象實作中那樣使用虛擬方法來計算面積或周長,您可以使用模式匹配轉至適當的公式來計算這些數量。 在下列範例中,會根據圖形,使用不同的公式來計算區域。
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)
輸出如下所示:
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
區別聯合可以是遞歸的,即是說,聯合本身可以包含在一或多個情況的類型中。 遞歸差異聯集可用來建立樹狀結構,用來建立程式設計語言中的表達式模型。 在下列程式代碼中,會使用遞歸差異聯集來建立二進位樹狀結構數據結構。 聯集包含兩個情況,Node
是具有整數值和左右子樹的節點,Tip
則是終止整個樹的。
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
在先前的程式代碼中,resultSumTree
具有值 10。 下圖顯示 myTree
的樹狀結構。
當樹狀結構中的節點具有異質性時,判別聯合運作良好。 在下列程式代碼中,類型 Expression
代表簡單程式設計語言中表達式的抽象語法樹狀結構,可支援加法和乘法數位和變數。 某些聯集案例不是遞歸的,代表數字(Number
)或變數(Variable
)。 其他案例具有遞歸性,並代表運算(Add
和 Multiply
),其中運算元也是表達式。
Evaluate
函式會使用比對表達式,以遞歸方式處理語法樹狀結構。
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
執行此程式代碼時,result
的值是 5。
F# 中的區別聯合可以相互遞歸,這表示多個聯合類型可以以遞歸方式相互參考。 當模型化階層式或互連結構時,這非常有用。 若要定義相互遞歸的判別聯合,請使用 and
關鍵詞。
例如,請考慮抽象語法樹狀結構 (AST) 表示法,其中表示式可以包含語句,而語句可以包含表達式:
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
有可能在歧視的聯集上定義成員。 下列範例示範如何定義 屬性並實作 介面:
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}"
由於 F# 9,歧視聯集會針對每個案例公開自動產生的 .Is*
屬性,讓您檢查某個值是否為特定案例。
這就是其使用方式:
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
下列屬性通常會在歧視聯集中看到:
[<RequireQualifiedAccess>]
[<NoEquality>]
[<NoComparison>]
[<Struct>]