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种模式,分别是:

  1. 常量模式
  2. 标识符模式
  3. 变量模式
  4. as模式
  5. OR模式
  6. AND模式
  7. cons模式
  8. 列表模式
  9. 数组模式
  10. 括号模式
  11. 元组模式
  12. 记录(Record)模式
  13. 通配符(_)模式
  14. 类型标记模式
  15. 类型测试模式
  16. 空(null)模式
  17. 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"

总结

  1. F#中的match语句是一个强大的工具,它可以用来匹配各种各样的模式。
  2. 模式匹配在现代语言中几乎是一个必备的特性,它可以保证程序的正确性,也可以让程序更加简洁。
  3. F#中还有一类更加复杂的模式,叫做活动模式(Active Patterns),这个在后面的文章中再介绍。
posted @ 2023-07-23 16:33  大福是小强  阅读(13)  评论(0编辑  收藏  举报  来源