F#奇妙游(19):F#中的模式匹配(match with)
分支选择
F#中的match语句是一个强大的工具,它可以用来匹配各种各样的模式。match语句的一般形式如下:
match expression with
| pattern1 [when condition1]-> result1
| pattern2 [when condition1] -> result2
| pattern3 [when condition1] -> result3
//...
| patternN -> resultN
| _ -> defaultResult
match语句的执行过程是这样的:首先,计算expression的值,然后将这个值与pattern1进行匹配,如果匹配成功,则返回result1,否则,将这个值与pattern2进行匹配,如果匹配成功,则返回result2,…,如果所有的选项都没有匹配上则抛出一个异常Microsoft.FSharp.Core.MatchFailureException
,并说明匹配不完整。实际上,IDE都会提醒匹配不完整的情况。这也是使用这个语言工具来编程时很重要的一个特性,可以保证程序的正确性。为了避免这个异常,可以在match语句的最后加上一个通配符模式_
,这样就可以保证所有的情况都被处理了。这个通配符的匹配方式不是一个很好的实践,还是应该尽量保证所有的情况都被处理。
在函数式的语言中,通常都有类似的语句,比如Haskell中的case语句,Scala中的match语句,Erlang中的case语句等等。Kotlin中也有类似的语句,叫做when语句。使用Match这类的语言机制,可以很方便的表达各种各样的模式,语法和语义都非常清晰。
F#中可以匹配的模式
F#中的模式匹配,帮助中一共列举了17种模式,分别是:
- 常量模式
- 标识符模式
- 变量模式
- as模式
- OR模式
- AND模式
- cons模式
- 列表模式
- 数组模式
- 括号模式
- 元组模式
- 记录(Record)模式
- 通配符(_)模式
- 类型标记模式
- 类型测试模式
- 空(null)模式
- nameof模式
在前面的F#奇妙游中,已经使用过多种匹配模式,例如在mergesort中,使用列表模式和cons模式可以很好处理列表的分解。另外,元组模式也很自然的将元组的各个部分分解出来,并根据元组各个部分的值或者类型进行匹配。
常量模式
常量模式中通常和枚举类型一起使用,例如:
type Color =
| Red = 0
| Green = 1
| Blue = 2
let printColor c =
match c with
| Color.Red -> printfn "Red"
| Color.Green -> printfn "Green"
| Color.Blue -> printfn "Blue"
枚举类型的值是常量,所以可以使用常量模式来匹配。
标识符模式
标识符模式最常用的场景是匹配option类型,例如:
let printOption (o: 'T option) =
match o with
| Some v -> printfn "Some %A" v
| None -> printfn "None"
这里Some和None都是标识符,可以用来匹配option类型的值。实际上,option本身也是一个DU类型,Some和None都是它的分支。
type 'T option =
| Some of 'T
| None
F#的这个option类型跟.NET中的null值在语义上很相似,但是在F#中可以定义一个Some null
。比如:
printOption (Some null)
这个语句可以正常执行,输出的结果是Some null
。
除了系统实现这个DU之外,还可以使用自己定义的DU并在match中分解各个部分,在前面介绍DU的奇妙游中,已经使用过。例如:
type Color =
| RGB of int * int * int
| RGBA of int * int * int * int
let printColor c =
match c with
| RGB(r, g, b) -> printfn "RGB(%d, %d, %d)" r g b
| RGBA(r, g, b, a) -> printfn "RGBA(%d, %d, %d, %d)" r g b a
当DU定义了名称的时候,可以使用名称来匹配,例如:
type Color =
| RGB of red:int * green:int * blue:int
| RGBA of red:int * green:int * blue:int * alpha:int
let printColor c =
match c with
| RGB(red=r; green=g; blue=b) -> printfn "RGB(%d, %d, %d)" r g b
| RGBA(red=r; green=g; blue=b; alpha=a) -> printfn "RGBA(%d, %d, %d, %d)" r g b a
这里的red, green, blue, alpha都是标识符模式,注意这里使用;
作为分隔。
printColor (RGB(100,100,100))
printColor (RGBA(100,100,100,100))
变量模式
变量模式可以匹配任何值,并将匹配的值绑定到变量上,例如:
let showTuple (x: 'T * 'T) =
match x with
| (a, b) when a > b -> printfn "%A > %A" a b
| (a, b) when a < b -> printfn "%A < %A" a b
| (a, b) -> printfn "%A = %A" a b
这里配合when条件,可以对匹配的值进行进一步的判断。
as模式
as模式是配合其它模式使用的,把匹配的值绑定到一个变量上。除了用在模式匹配上,还能用于let绑定上,例如:
let (a, b) as t = (1, 2)
这里的t就是一个元组,值为(1, 2)。
OR模式和AND模式
OR模式和AND模式都是用来组合模式的,OR模式用于匹配多个模式中的一个,AND模式用于匹配多个模式同时满足的情况。例如:
let matchTwoValues a b =
match a, b with
| (1, _) & (_, 2) -> "both matched"
| (_, 2) | (1, _) -> "matched"
| _ -> "not matched"
这里的|
表示OR模式,&
表示AND模式。值得注意的是,|也是match定义分支的符号,这里|又表示OR模式,就跟C语言中的switch语句一样,匹配上一项又继续匹配下一项,最要有一个匹配上,就执行对应的分支。
集合的匹配模式
针对集合,F#提供了一些特殊的匹配模式,cons模式,列表模式,数组模式,括号模式。
在函数式编程的语言中,列表是一个非常重要的数据结构。甚至有整个语言的语法和数据结构全部建立在列表之上的,比如Lisp。在F#中,列表也是一个非常重要的数据结构,它是一个不可变的数据结构,可以使用cons操作符::
来构造列表,也可以使用[]
来构造列表。例如:
在模式匹配中,也包含了对列表的匹配,例如:
let rec sumList l =
match l with
| [] -> 0
| h :: t -> h + sumList t
通过::
,把列表的第一个元素和剩余的元素分开,这里的h和t都是变量模式,可以匹配任何值。这个函数的功能是计算列表中所有元素的和,这个思路也是函数式编程中最重要迭代思想和分治思想的体现。
对于数组,也可以使用类似的方式进行匹配,例如:
let sumArray a =
match a with
| [| |] -> 0
| [| h |] -> h
| [| h; t |] -> h + t
| _ -> 0
这里的[| |]
表示空数组,[| h |]
表示只有一个元素的数组,[| h; t |]
表示有超过一个元素的数组,_
表示其它情况。
最后在匹配列表的时候,还可以使用括号模式,例如:
let cv list value =
let rec cl list acc =
match list with
| (elm1 & head) :: tail when elm1 = value -> cl tail (acc+1)
| head :: tail -> cl tail acc
| [] -> acc
cl list 0
这里的(elm1 & head)
就是括号模式,我还有点不明白为什么要这么麻烦,直接用head作为绑定变量俩做判断貌似也是可以的。
> let cv list value =
- let rec cl list acc =
- match list with
- | (elm1 & head) :: tail when elm1 = value -> cl tail (acc+1)
- | head :: tail -> cl tail acc
- | [] -> acc
- cl list 0;;
val cv: list: 'a list -> value: 'a -> int when 'a: equality
> cv [for x in -10..10 -> x* x-4] 0;;
val it: int = 2
记录模式
对于记录类型,可以使用记录模式来匹配,例如:
type Person = { Name: string; Age: int }
let printPerson p =
match p with
| { Person.Name = "Tom"; Person.Age = 20 } -> printfn "[!]Tom is 20"
| { Person.Name = "Jerry"; Person.Age = 18 } -> printfn "[!]Jerry is 18"
| { Person.Name = name; Person.Age = age } -> printfn "%s is %d" name age
类型匹配模式
类型匹配模式用于匹配类型,例如:
let printType x =
match box x with
| :? int -> printfn "int"
| :? string -> printfn "string"
| :? float -> printfn "float"
| _ -> printfn "other"
这里的:?
表示类型匹配模式,box
表示将值转换为object类型,这里的box x
表示将x转换为object类型,这样就可以使用:?
来匹配类型了。如果没有box x
,就需要在函数签名中显示的声明类型,例如:
let printType (x: obj) =
match x with
| :? int -> printfn "int"
| :? string -> printfn "string"
| :? float -> printfn "float"
| _ -> printfn "other"
总结
- F#中的match语句是一个强大的工具,它可以用来匹配各种各样的模式。
- 模式匹配在现代语言中几乎是一个必备的特性,它可以保证程序的正确性,也可以让程序更加简洁。
- F#中还有一类更加复杂的模式,叫做活动模式(Active Patterns),这个在后面的文章中再介绍。