模式匹配
所有程序都需要通过筛选和排序数据;在函数编程中做这些需要用到模式匹配。模式匹配有点像C#和C++中的switch,但是它比switch更强大。模式匹配就是根据输入的数据匹配定义好的一系列规则,如果匹配成功,则执行。该模式匹配表达式然后返回认为是符合规则的结果,因此,在模式匹配的所有规则必须返回相同的类型。
如果要执行模式匹配,你可以使用match和with关键字,并在在with后面利用->列出一系列的匹配规则,下面的代码段利用模式匹配规则来模仿了if表达式,第一条匹配规则是对比true值,如果该规则匹配,它将打印“x is odd"到控制台:
> let isOdd x = (x % 2 = 1)
- let describeNumber x =
- match isOdd x with
- | true -> printf "x is odd"
- | false -> printf "x is even";;
val isOdd : int -> bool
val describeNumber : int -> unit
> describeNumber 4;;
x is evenval it : unit = ()
在模式匹配中,最简单的就是利用常数值来进行匹配了。例如下面的例子就构造了一对真值表,通过一对布尔元组来进行匹配:
> let testAnd x y =
- match x, y with
- | true, true -> true
- | true, false -> false
- | false, false -> false;
- | false, true -> false;;
val testAnd : bool -> bool -> bool
> testAnd true true;;
val it : bool = true
> testAnd true false;;
val it : bool = false
注意的是,类型推断同样适用于模式匹配中,在上面的示例中,因为模式匹配规则中x的值为bool型,而y值也是bool行,所以可以根据x和y的类型推断出匹配的类型。
下划线"_"是一个通配符匹配的事情。因此,你可以简化以前例如使用通配符捕获任何输入,除了true,true外:
> testAnd true false;;
val it : bool = false
> let testAnd x y =
- match x, y with
- | true, true -> true
- | _, _ -> false;;
val testAnd : bool -> bool -> bool
> testAnd true false;;
val it : bool = false
> testAnd true true;;
val it : bool = true
模式匹配的规则是按照顺序匹配的,所以如果你一旦匹配通过后,这将不会在执行下面的匹配,所以,这就要求我们在实践中把一些最通用的放在最后面,把特例匹配放在前面,就比如上面的"_,_",如果放在第一位,那么无论你输入什么值,都只会返回false。
Match Failure
你可以会很好奇,如果我们把上面的函数testAnd中把可能的一对匹配规则排除掉,将会发生什么呢?例如:
> let testAnd x y =
- match x, y with
- | true, true -> true
- | true, false -> false
- | false, false -> false;;
同时,如果没有找到匹配的模式匹配时,一个类型为Microsoft.FSharp.Core.MatchFailureException的异常将会抛出。您可以避免通过确保所有可能的情况都包括了这方面。幸运的是,幸运的是,F#编译器会发出警告当能确定的模式匹配规则是不完整的:
编译后将提示一个警告:
match x, y wih
----------^^^^
stdin(23,11): warning FS0025: Incomplete pattern matches on this expression. For
example, the value '(_,true)' may indicate a case not covered by the pattern(s)
Named Patterns
我们刚刚介绍了配对常数值,但你也可以使用命名模式来提取数据并将其绑定到一个新值来进行匹配。考虑下面的示例,它针对一个特定的字符串进行匹配,但除了特定字符串之外的任何其他值,它都绑定到一个新值x:
> let greet name =
- match name with
- | "Robert" -> printf "Hello,Bob"
- | "William" -> printf "Hello,Bill"
- | x -> printfn "Hello,%s" x;;
val greet : string -> unit
> greet "Earl";;
Hello,Earl
val it : unit = ()
> greet "Robert";;
Hello,Bobval it : unit = ()
Matching Literals
命名模式是伟大的, 但同时它也阻止你使用现有值最为作为一个模式匹配的一部分。在前面的例子,作为硬编码模式匹配部分的名称,将使代码更难维持并可能导致错误。
如果你想匹配一个众所周知的价值,但并不想复制并粘贴值在每个匹配规则中, 你可能会使用命名模式,也即是先申明一个字面值,然后在用到的地方用字面值来替换,但是这个是错误的用法,看下面的示例:
let bill = "Bill Gates"
let greet name =
match name with
| bill -> "Hello Bill!"
| x -> sprintf "Hello, %s" x;;
| x -> sprintf "Hello, %s" x;;
------^^
stdin(56,7): warning FS0026: This rule will never be matched.
val bill : string = "Bill Gates"
val greet : string -> string
在上面的示例中,第一个模式匹配规则并不比较值name指向值bill,而只是引入了一个新值bill,所以这里的bill与第一行申请的bill是完全不同的概念。这就是为什么第二个模式匹配规则产生一个警告,因为先前的规则已经捕获所有模式匹配的输入。为了配合对一个现有的值,您必须添加[<Literal>]属性,它允许任何文字值(常数),用于内部模式匹配。请注意,使用[<Literal>]标记必须第一个字母大写。例如:
>
- [<Literal>]
- let Bill = "Bill Gates";;
val Bill : string = "Bill Gates"
> let greet name =
- match name with
- | Bill -> "Hello Bill"
- | x -> sprintf "Hello %s" x;;
val greet : string -> string
> greet "Bill G.";;
val it : string = "Hello Bill G."
> greet "Bill Gates";;
val it : string = "Hello Bill"
只有整数,字符,布尔,字符串和浮点数可以标记为字面值,如果你想对更复杂的匹配类型,如字典或映射,则必须使用when关键字。
when Guards
虽然模式匹配是一个强有力的概念,有时你需要自定义逻辑确定规则是否应该匹配,这时候就要用到when匹配模式。在一个模式匹配中,如果when表达式为true,则代码就会被执行。下面的示例实现了一个简单的猜随机数字的游戏,when匹配模式将用来检查猜测的数字是否高于,低于或等于随机产品的数字:
let highLowGame () =
let rng = new Random()
let secretNumber = rng.Next() % 100
let rec highLowGameStep () =
printfn "Guess the secret number:"
let guessStr = Console.ReadLine()
let guess = Int32.Parse(guessStr)
match guess with
| _ when guess > secretNumber
-> printfn "The secret number is lower."
highLowGameStep()
| _ when guess = secretNumber
-> printfn "You've guessed correctly!"
()
| _ when guess < secretNumber
-> printfn "The secret number is higher."
highLowGameStep()
highLowGameStep();;
运入效果如下:
> highLowGame();;
Guess the secret number: 50
The secret number is lower.
Guess the secret number: 25
The secret number is higher.
Guess the secret number: 37
You've guessed correctly!
val it : unit = ()
Grouping Patterns
当你的模式匹配包含越来越多的规则,你可能想合并模式,这里有两种模式合并的方法。第一种是或模式("|"来代替),它将多个匹配模式结合起来,如果其中的任何一个匹配,则将执行。第二种方法是与模式("&"来代替),将多个匹配规则结合起来,并且进行匹配时,要全部满足匹配规则,才执行。下面两个示例分别说明了或、与匹配的规则:
let vowelTest c =
match c with
| 'a' | 'e' | 'i' | 'o' | 'u'
-> true
| _ -> false
let describeNumbers x y =
match x, y with
| 1, _
| _, 1
-> "One of the numbers is one."
| (2, _) & (_, 2)
-> "Both of the numbers are two"
| _ -> "Other."
与匹配模式在正常模式匹配中很少使用,当使用主动模式时,它是非常有用的。
Matching the Structure of Data
模式匹配也可以利用数据结构做匹配。
Tuples
你已经看到如何匹配元组,如果元组的元素均是由逗号分隔在模式匹配中,每个元素将单独匹配。但是,如果输入的元组中使用命名模式,
绑定的价值将有一个元组类型。
在下面的例子,第一个规则绑定值元组,它获取值x和y, 其他规则与元组元素单独匹配
> let testXor x y =
- match x, y with
- | tuple when fst tuple = snd tuple
- -> true
- | true, true -> false
- | false, false -> false;;
Lists
下面的示例演示了如果利用列表结构来做匹配。函数listLength匹配列表的固定大小,对于匹配为成功的则递归调用列表本身的尾部:
> let rec listLength l =
- match l with
- | [] -> 0
- | [_] -> 1
- | [_; _] -> 2
- | [_; _; _] -> 3
- | hd :: tail -> 1 + listLength tail;;
val listLength : 'a list -> int
> listLength [2;5;3;4;6;7;9];;
val it : int = 7
前四个模式匹配规则匹配列表的具体长度,使用通配符代表列表中的元素并不重要。该模式匹配的最后一个规则,用操作符::来匹配列表的第一个元素hd,tail则为除了第一个元素外剩下的元素,它可以是一个空列表[]。
Options
模式匹配还提供了一个功能更强大的方法来使用Option类型:
let describeOption o =
match o with
| Some(42) -> "The answer was 42, but what was the question?"
| Some(x) -> sprintf "The answer was %d" x
| None -> "No answer found."
Outside of Match Expressions
模式匹配是一个非常强大的,但其最好的方面是,它不必须用'matc with‘表达。匹配模式发生在整个F#语言中,比如下面的一些就是。
let bindings
let 绑定事实上也是一种匹配规则,所以如果你想下面这样写:
let x = f()
你就可以认为它可以像你在下面写的这样:
match f() with
| x -> ...
这也就是为什么我们可以利用绑定来提取元组的值:
// This...
let x, y = (100, 200)
// ... is the same as this...
match (100, 200) with
| x, y -> ...
Wildcard patterns
想象一下,你想编写一个函数, 但并不关心其中的某些参数。或者一个元组中的值。在这种情况下,可以使用通配符模式:
> List.iter (fun _ -> printfn "Step...") [1 .. 3];;
Step...
Step...
Step...
val it : unit = ()
> let _, second, _ = (1, 2, 3);;
val second : int = 2