可区分联合 (F#)
可区分联合支持具有以下特点的值:它可能属于多个命名用例中的一个,而每个命名用例的值和类型可能各不相同。 可区分联合对于以下数据很有用:异类数据;可有特殊用例(包括有效用例和错误用例)的数据;每个实例的类型不同的数据;可区分联合还可用作小对象层次结构的备用项。 此外,递归的可区分联合用于表示树数据结构。
type type-name =
| case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]
| case-identifier2 [of [fieldname3 : ]type3 [ * [ fieldname4 : ]type4 ...]
...
备注
可区分联合与其他语言的联合类型相似,但也存在不同。 与 C++ 中的联合类型或 Visual Basic 中的 Variant 类型一样,存储在值中的数据不是固定的;它可以是若干个不同选项中的一个。 与这些其他语言中的联合不同,将为每个可能的选项提供一个用例标识符。 用例标识符是此类型的对象可能属于的各种值类型的名称;值是可选的。 如果值不存在,则此用例等效于枚举用例。 如果值存在,则每个值既可以是指定类型的单个值,也可以是聚合相同类型或不同类型的多个字段的元组。 自 F# 3.1,您可以为单个字段命名,但该名称是可选的,即使同一用例中的其他字段都有名称。
例如,考虑形状类型的下列声明。
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
上面的代码声明一个可识别的联合形状,该形状能够拥有三种情况的值:矩形、圆形和棱镜。 每种情况都具有不同的字段集。 矩形用例有两个命名字段,两个都是 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 类型指定一个既可以具有某类型的值也可以没有任何值的泛型类型。 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 特性应用于联合类型定义。
使用可区分联合替代对象层次结构
通常可以将可区分联合用作小对象层次结构的更简单的替代项。 例如,下面的可区分联合可用于替代具有圆形、方形等的派生类型的 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。