上一章講述了如何實現自定義的提取器以及如何在模式匹配中使用它們,但是只討論了如何從給定的數據結構中分解固定數目的參數。對某種數據結構來說,Scala 提供了提取任意多個參數的模式匹配方法。
比如,你可以匹配只有兩個、或者只有三個元素的列表:
~~~
val xs = 3 :: 6 :: 12 :: Nil
xs match {
case List(a, b) => a * b
case List(a, b, c) => a + b + c
case _ => 0
}
~~~
除此之外,也可以使用通配符 `_*` 匹配長度不確定的列表:
~~~
val xs = 3 :: 6 :: 12 :: 24 :: Nil
xs match {
case List(a, b, _*) => a * b
case _ => 0
}
~~~
這個例子中,第一個模式成功匹配,把 `xs` 的前兩個元素分別綁定到 `a` 、`b` ,而剩余的列表,無論其還有多少個元素,都直接被忽略掉。
顯然,這種模式的提取器是無法通過上一章介紹的方法來實現的。需要一種特殊的方法,來使得一個提取器可以接受某一類型的對象,將其解構成列表,且這個列表的長度在編譯期是不確定的。
`unapplySeq` 就是用來做這件事情的,下面的代碼是其可能的方法簽名:
~~~
def unapplySeq(object: S): Option[Seq[T]]
~~~
這個方法接受類型 `S` 的對象,返回一個類型參數為 `Seq[T]` 的 `Option` 。
### 例子:提取給定的名字
現在我們舉一個例子來展示如何使用這種提取器。
假設有一個應用,其某處代碼接收了一個表示人名且類型為 `String` 的參數,這個字符串可能包含了這個人的第二個甚至是第三個名字(如果這個人不止有一個名字)。比如說, `Daniel` 、 `Catherina Johanna` 、 `Matthew John Michael` 。而我們想做的是,從這個字符串中提取出單個的名字。
下面的代碼是一個用 `unapplySeq` 方法實現的提取器:
~~~
object GivenNames {
def unapplySeq(name: String): Option[Seq[String]] = {
val names = name.trim.split(" ")
if (name.forall(_.isEmpty)) None
else Some(names)
}
}
~~~
給定一個含有一個或多個名字的字符串,這個提取器會將其解構成一個列表。如果字符串不包含有任何名字,提取器會返回 `None` ,提取器所在的那個模式就匹配失敗。
下面對提取器進行測試:
~~~
def greetWithFirstName(name: String) = name match {
case GivenNames(firstName, _*) => "Good morning, $firstname!"
case _ => "Welcome! Please make sure to fill in your name!"
}
~~~
`greetWithFirstName("Daniel")` 會返回 "Good morning, Daniel!",而 `greetWithFirstName("Catherina Johanna")` 會返回 "Good morning, Catherina!"。
### 固定和可變的參數提取
有些時候,需要提取出至少多少個值,這樣,在編譯期,就知道必須要提取出幾個值出來,再外加一個可選的序列,用來保存不確定的那一部分。
在我們的例子中,假設輸入的字符串包含了一個人完整的姓名,而不僅僅是名字。比如字符串可能是"John Doe"、"Catherina Johanna Peterson",其中,"Doe"、"Peterson"是姓,"John"、"Catherina"、"Johanna"是名。我們想做的是匹配這樣的字符串,把姓綁定到第一個變量,把第一個名字綁定到第二個變量,第三個變量存放剩下的任意個名字。
稍微修改 `unapplySeq` 方法就可以解決上述問題:
~~~
def unapplySeq(object: S): Option[(T1, .., Tn-1, Seq[T])]
~~~
`unapplySeq` 返回的同樣是 `Option[TupleN]` ,只不過,其最后一個元素是一個 `Seq[T]` 。這個方法簽名看起來應該很熟悉,它和之前的一個 `unapply` 簽名類似。
下列代碼是利用這個方法生成的提取器:
~~~
object Names {
def unapplySeq(name: String): Option[(String, String, Seq[String])] = {
val names = name.trim.split(" ")
if (names.size < 2) None
else Some((names.last, names.head, names.drop(1).dropRight(1)))
}
}
~~~
仔細看看其返回值,及其構造 `Some` 的方式。代碼返回一個類型參數為 `Tuple3` 的 `Option` ,這個元組包含了姓、名、以及由剩余的名字構成的序列。
如果這個提取器用在一個模式中,那只有當給定的字符串至少含有姓和名時,模式才匹配成功。
下面用這個提取器重寫 `greeting` 方法:
~~~
def greet(fullName: String) = fullName match {
case Names(lastName, firstName, _*) =>
"Good morning, $firstName $lastName!"
case _ =>
"Welcome! Please make sure to fill in your name!"
}
~~~
你可以在 REPL 中或者 worksheet 上試試這些代碼。
### 小結
這一章里,我們學會了怎樣去實現和使用返回不定長度值序列的提取器。提取器是一個相當強大的工具,你可以靈活的重用它們,從而提供一種有效的方法來擴展要匹配的模式。
下一章,我會給出模式匹配在 Scala 中的不同用法(現在所見到的只是冰山一角)。