閱讀英文版本

分享方式:


歧視聯集

判別聯集為可以是多個具名案例之一的值提供支援,可能每個案例都有不同的值和類型。 判別聯合對於異質數據非常有用;這類數據可以包含特殊情況,包括有效和錯誤的案例;類型隨實例不同而變化的數據;以及作為小型物件階層的替代方案。 此外,遞迴判別聯集可用來表示樹形資料結構。

語法

F#
[ 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 類型的宣告。

F#
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 案例有三個字段,其中兩個字段(寬度和高度)是命名字段。 未命名的欄位稱為匿名欄位。

您可以根據下列範例,提供具名和匿名欄位的值來建構物件。

F#
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 類型宣告如下。

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

上一個程式代碼指定類型 Option 是一個包含兩個情況的區分聯合,分別是 SomeNoneSome 案例具有相關聯的值,其中包含一個匿名欄位,其類型是由類型參數 'a表示。 None 案例沒有相關聯的值。 因此,option 型別會指定具有某些型別值或無值的泛型型別。 類型 Option 也有小寫類型別名,option,較常用。

案例識別碼可以作為區分聯合型別的建構函式使用。 例如,下列程式代碼可用來建立 option 類型的值。

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

案例標識碼也會用於模式比對表達式中。 在模式比對表達式中,會為與個別案例相關聯的值提供標識碼。 例如,在下列程式代碼中,x 是標識符,指定與 option 類型 Some 案例相關聯的值。

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

在模式比對表達式中,您可以使用具名欄位來指定區分聯集的比對。 針對先前宣告的 Shape 類型,您可以使用具名欄位,如下列程式代碼所示,來擷取欄位的值。

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

通常,案例識別碼可以直接使用,不需要附帶聯合體的名稱。 如果您希望名稱總是添加聯合名稱限定,您可以將 RequireQualifiedAccess 屬性套用至聯合類型定義。

解構可判別聯合型別

在 F# 中,歧視聯集常用於領域建模,包裝單一類型。 也很容易透過模式比對來擷取基礎值。 您不需要針對單一案例使用比對表示式:

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

下列範例示範了這一點:

F#
type ShaderProgram = | ShaderProgram of id:int

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

函式參數中也直接允許模式匹配,因此您可以在其中處理個別案例:

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

結構化辨別聯集

您也可以將可辨識聯合體表示為結構。 這會使用 [<Struct>] 屬性來完成。

F#
[<Struct>]
type SingleCase = Case of string

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

因為這些是實值型別,而不是參考型別,因此相較於參考區分聯集,需要額外的考量:

  1. 它們會複製為實值型別,並具有實值型別語意。
  2. 您無法使用遞歸類型定義搭配多寫結構區分等位。

在 F# 9 之前,在聯合體中每個個案都必須指定一個唯一的個案名稱。 從 F# 9 開始,會解除限制。

使用歧視聯集,而不是物件階層

您通常可以使用歧視聯集做為較簡單的小型物件階層替代方案。 例如,可以使用下列區分聯集,而不是具有圓形、正方形等衍生型別的 Shape 基類。

F#
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

與其像在面向對象實作中那樣使用虛擬方法來計算面積或周長,您可以使用模式匹配轉至適當的公式來計算這些數量。 在下列範例中,會根據圖形,使用不同的公式來計算區域。

F#
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則是終止整個樹的。

F#
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的樹狀結構。

顯示 myTree 之樹狀結構的圖表。

當樹狀結構中的節點具有異質性時,判別聯合運作良好。 在下列程式代碼中,類型 Expression 代表簡單程式設計語言中表達式的抽象語法樹狀結構,可支援加法和乘法數位和變數。 某些聯集案例不是遞歸的,代表數字(Number)或變數(Variable)。 其他案例具有遞歸性,並代表運算(AddMultiply),其中運算元也是表達式。 Evaluate 函式會使用比對表達式,以遞歸方式處理語法樹狀結構。

F#
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) 表示法,其中表示式可以包含語句,而語句可以包含表達式:

F#
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

成員

有可能在歧視的聯集上定義成員。 下列範例示範如何定義 屬性並實作 介面:

F#
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* 屬性

由於 F# 9,歧視聯集會針對每個案例公開自動產生的 .Is* 屬性,讓您檢查某個值是否為特定案例。

這就是其使用方式:

F#
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>]

另請參閱