已區分的聯集 (F#)
「已區分的聯集」(Discriminated Union) 支援各種具名案例之一的值,而每個案例可能都有不同的值和類型。 已區分的聯集適用於異質資料、可具有特殊案例的資料 (包括有效和錯誤案例)、不同執行個體之類型不同的資料,以及做為小型物件階層的替代項目。 此外,遞迴的已區分聯集是用來代表樹狀資料結構。
type type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
...
備註
已區分的聯集與其他語言的聯集類型類似,但是有差異。 與 C++ 中的聯集類型或 Visual Basic 中的變數類型相同,儲存在值中的資料並未固定;它可以是多個相異選項的其中一個。 不過,與其他語言的聯集不同的是,每個可能選項都會指定「案例識別項」(Case Identifier)。 案例識別項是此類型之物件可能的各種實值類型的名稱,這些值為選擇性。 如果值不存在,則案例就相當於列舉案例。 如果值存在,則每個值都可以是指定之類型的單一值,或是彙總多個相同或不同類型之欄位的 Tuple。 自 F# 3.1 之後,您可以為個別欄位指定名稱,不過,名稱是選擇性的,即使同一個案例中的其他欄位已命名也一樣。
例如,考慮下列 Shape 類型的宣告:
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
上述程式碼宣告差別等位圖形,可能有下列三個案例之一的值:矩形、圓形和角柱。 每個案例都有一組不同的欄位。 矩形案例有兩個分別名為 width 和 length 的具名欄位,都屬於 float 類型。 圓形案例只有一個具名欄位,即半徑。 角柱案例有三個欄位,其中兩個名為 [未命名] 的欄位稱為匿名欄位。
您可以根據下列範例,提供具名和匿名欄位的值來建構物件。
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 類型指定值為 some 類型或沒有值的泛型類型。 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."
在模式比對運算式中,您可以使用具名欄位來指定差別聯集比對。 對於先前宣告的 [圖形] 類型,您可以使用具名欄位擷取欄位的值,如下列程式碼所示。
let getShapeHeight shape =
match shape with
| Rectangle(height = h) -> h
| Circle(radius = r) -> 2. * r
| Prism(height = h) -> h
一般而言,可以使用案例識別項,而不需要使用聯集名稱來限定它們。 如果您想要一律使用聯集名稱來限定名稱,則可以將 RequireQualifiedAccess 屬性套用至聯集類型定義。
使用已區分的聯集,而非物件階層
您可以經常使用已區分的聯集做為小型物件階層的簡單替代項目。 例如,可以使用下列已區分的聯集,而非具有 circle、square 等項目之衍生類型的 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 的樹狀結構。
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.ofList [ "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。