在 Coursera 上,想必你遇到過一個非常強大的語言特性:模式匹配 。它可以解綁一個給定的數(shù)據(jù)結(jié)構(gòu)。這不是 Scala 所特有的,在其他出色的語言中,如 Haskell、Erlang,模式匹配也扮演著重要的角色。
模式匹配可以解構(gòu)各種數(shù)據(jù)結(jié)構(gòu),包括 列表 、 流 ,以及 樣例類 。但只有這些數(shù)據(jù)結(jié)構(gòu)才能被解構(gòu)嗎,還是可以用某種方式擴展其使用范圍?而且,它實際是怎么工作的?是不是有什么魔法在里面,得以寫些類似下面的代碼?
case class User(firstName: String, lastName: String, score: Int)
def advance(xs: List[User]) = xs match {
case User(_, _, score1) :: User(_, _, score2) :: _ => score1 - score2
case _ => 0
}
事實證明沒有什么魔法,這都歸功于提取器 。
提取器使用最為廣泛的使用有著與 構(gòu)造器 相反的效果:構(gòu)造器從給定的參數(shù)列表創(chuàng)建一個對象,而提取器卻是從傳遞給它的對象中提取出構(gòu)造該對象的參數(shù)。Scala 標準庫包含了一些預(yù)定義的提取器,我們會大致的了解一下它們。
樣例類非常特殊,Scala會自動為其創(chuàng)建一個 伴生對象 :一個包含了 apply
和 unapply
方法的 單例對象 。apply
方法用來創(chuàng)建樣例類的實例,而 unapply
需要被伴生對象實現(xiàn),以使其成為提取器。
unapply
方法可能不止有一種方法簽名,不過,我們從只有最簡單的開始,畢竟使用更廣泛的還是只有一種方法簽名的 unapply
。假設(shè)要創(chuàng)建了一個 User
特質(zhì),有兩個類繼承自它,并且包含一個字段:
trait User {
def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User
我們想在各自的伴生對象中為 FreeUser
和 PremiumUser
類實現(xiàn)提取器,就像 Scala 為樣例類所做的一樣。如果想讓樣例類只支持從給定對象中提取單個參數(shù),那 unapply
方法的簽名看起來應(yīng)該是這個樣子:
def unapply(object: S): Option[T]
這個方法接受一個類型為 S
的對象,返回類型 T
的 Option
, T
就是要提取的參數(shù)類型。
在Scala中,
Option
是null
值的安全替代。以后會有一個單獨的章節(jié)來講述它,不過現(xiàn)在,只需要知道,unapply
方法要么返回Some[T]
(如果它能成功提取出參數(shù)),要么返回None
,None
表示參數(shù)不能被unapply
具體實現(xiàn)中的任一提取規(guī)則所提取出。
下面的代碼是我們的提取器:
trait User {
def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User
object FreeUser {
def unapply(user: FreeUser): Option[String] = Some(user.name)
}
object PremiumUser {
def unapply(user: PremiumUser): Option[String] = Some(user.name)
}
現(xiàn)在,可以在REPL中使用它:
scala> FreeUser.unapply(new FreeUser("Daniel"))
res0: Option[String] = Some(Daniel)
如果調(diào)用返回的結(jié)果是 Some[T]
,說明提取模式匹配成功,如果是 None
,說明模式不匹配。
一般不會直接調(diào)用它,因為用于提取器模式時,Scala 會隱式的調(diào)用提取器的 unapply
方法。
val user: User = new PremiumUser("Daniel")
user match {
case FreeUser(name) => "Hello" + name
case PremiumUser(name) => "Welcome back, dear" + name
}
你會發(fā)現(xiàn),兩個提取器絕不會都返回 None
。這個例子展示的提取器要比之前所見的更有意義。如果你有一個類型不確定的對象,你可以同時檢查其類型并解構(gòu)。
這個例子里, FreeUser
模式并不會匹配,因為它接受的類型和我們傳遞給它的不一樣。這樣一來, user
對象就會被傳遞給第二個模式,也就是 PremiumUser
伴生對象的 unapply
方法。而這個模式會匹配成功,從而返回值就被綁定到 name
參數(shù)上。
在接下來的文章里,我們會看到一個并不總是返回 Some[T]
的提取器的例子。
現(xiàn)在,假設(shè)類有多個字段:
trait User {
def name: String
def score: Int
}
class FreeUser(
val name: String,
val score: Int,
val upgradeProbability: Double
) extends User
class PremiumUser(
val name: String,
val score: Int
) extends User
如果提取器想解構(gòu)出多個參數(shù),那它的 unapply
方法應(yīng)該有這樣的簽名:
def unapply(object: S): Option[(T1, ..., T2)]
這個方法接受類型為 S
的對象,返回類型參數(shù)為 TupleN
的 Option
實例,TupleN
中的 N
是要提取的參數(shù)個數(shù)。
修改類之后,提取器也要做相應(yīng)的修改:
trait User {
def name: String
def score: Int
}
class FreeUser(
val name: String,
val score: Int,
val upgradeProbability: Double
) extends User
class PremiumUser(
val name: String,
val score: Int
) extends User
object FreeUser {
def unapply(user: FreeUser): Option[(String, Int, Double)] =
Some((user.name, user.score, user.upgradeProbability))
}
object PremiumUser {
def unapply(user: PremiumUser): Option[(String, Int)] =
Some((user.name, user.score))
}
現(xiàn)在可以拿它來做模式匹配了:
val user: User = new FreeUser("Daniel", 3000, 0.7d)
user match {
case FreeUser(name, _, p) =>
if (p > 0.75) "$name, what can we do for you today?"
else "Hello $name"
case PremiumUser(name, _) =>
"Welcome back, dear $name"
}
有些時候,進行模式匹配并不是為了提取參數(shù),而是為了檢查其是否匹配。這種情況下,第三種 unapply
方法簽名(也是最后一種)就有用了,這個方法接受 S
類型的對象,返回一個布爾值:
def unapply(object: S): Boolean
使用的時候,如果這個提取器返回 true
,模式會匹配成功,否則,Scala 會嘗試拿 object
匹配下一個模式。
上一個例子存在一些邏輯代碼,用來檢查一個免費用戶有沒有可能被說服去升級他的賬戶。其實可以把這個邏輯放在一個單獨的提取器中:
object premiumCandidate {
def unapply(user: FreeUser): Boolean = user.upgradeProbability > 0.75
}
你會發(fā)現(xiàn),提取器不一定非要在這個類的伴生對象中定義。正如其定義一樣,這個提取器的使用方法也很簡單:
val user: User = new FreeUser("Daniel", 2500, 0.8d)
user match {
case freeUser @ premiumCandidate() => initiateSpamProgram(freeUser)
case _ => sendRegularNewsletter(user)
}
使用的時候,只需要把一個空的參數(shù)列表傳遞給提取器,因為它并不真的需要提取數(shù)據(jù),自然也沒必要綁定變量。
這個例子有一個看起來比較奇怪的地方:我假設(shè)存在一個空想的 initiateSpamProgram
函數(shù),其接受一個 FreeUser
對象作為參數(shù)。模式可以與任何一種 User
類型的實例進行匹配,但 initiateSpamProgram
不行,只有將實例強制轉(zhuǎn)換為 FreeUser
類型, initiateSpamProgram
才能接受。
因為如此,Scala 的模式匹配也允許將提取器匹配成功的實例綁定到一個變量上,這個變量有著與提取器所接受的對象相同的類型。這通過 @
操作符實現(xiàn)。premiumCandidate
接受 FreeUser
對象,因此,變量 freeUser
的類型也就是 FreeUser
。
布爾提取器的使用并沒有那么頻繁(就我自己的情況來說),但知道它存在也是很好的,或遲或早,你會遇到一個使用布爾提取器的場景。
解構(gòu)列表、流的方法與創(chuàng)建它們的方法類似,都是使用 cons 操作符: ::
、 #::
,比如:
val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
case first #:: second #:: _ => first - second
case _ => -1
}
你可能會對這種做法產(chǎn)生困惑。除了我們已經(jīng)見過的提取器用法,Scala 還允許以中綴方式來使用提取器。所以,我們可以寫成 e(p1, p2)
,也可以寫成 p1 e p2
,其中 e
是提取器, p1
、 p2
是要提取的參數(shù)。
同樣,中綴操作方式的 head #:: tail
可以被寫成 #::(head, tail)
,提取器 PremiumUser
可以這樣使用: name PremiumUser score
。當然,這樣做并沒有什么實踐意義。一般來說,只有當一個提取器看起來真的像操作符,才推薦以中綴操作方式來使用它。所以,列表和流的 cons
操作符一般使用中綴表達,而 PreimumUser
則不用。
盡管 #::
提取器在模式匹配中的使用并沒有什么特殊的,但是,為了更好的理解上面的代碼,還是進一步來分析一下。而且,這是一個很好的例子,根據(jù)要匹配的數(shù)據(jù)結(jié)構(gòu)的狀態(tài),提取器很可能返回 None
。
如下是 Scala 2.9.2 源代碼中完整的 #::
提取器代碼:
object #:: {
def unapply[A](xs: Stream[A]): Option[(A, Stream[A]) =
if (xs.isEmpty) None
else Some((xs.head, xs.tail))
}
如果給定的流是空的,提取器就直接返回 None
。因此, case head #:: tail
就不會匹配任何空的流。否則,就會返回一個 Tuple2
,其第一個元素是流的頭,第二個元素是流的尾,尾本身又是一個流。這樣, case head #:: tail
就會匹配有一個或多個元素的流。如果只有一個元素, tail
就會被綁定成空流。
為了理解流提取器是怎么在模式匹配中工作的,重寫上面的例子,把它從中綴寫法轉(zhuǎn)成普通的提取器模式寫法:
val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
case #::(first, #::(second, _)) => first - second
case _ => -1
}
首先為傳遞給模式匹配的初始流 xs
調(diào)用提取器。由于提取器返回 Some(xs.head, xs.tail)
,從而 first
會綁定成 58,xs
的尾會繼續(xù)傳遞給提取器,提取器再一次被調(diào)用,返回首和尾, second
就被綁定成 43
,而尾就綁定到通配符 _
,被直接扔掉了。
那到底該在什么時候使用、怎么使用自定義的提取器呢?尤其考慮到,使用樣例類就能自動獲得可用的提取器。
一些人指出,使用樣例類、對樣例類進行模式匹配打破了封裝,耦合了匹配數(shù)據(jù)和其具體實現(xiàn)的方式,這種批評通常是從面向?qū)ο蟮慕嵌瘸霭l(fā)的。如果想用 Scala 進行函數(shù)式編程,將樣例類當作只包含純數(shù)據(jù)(不包含行為)的代數(shù)數(shù)據(jù)類型 ,那它非常適合。
通常,只有當從無法掌控的類型中提取數(shù)據(jù),或者是需要其他進行模式匹配的方法時,才需要實現(xiàn)自己的提取器。
提取器的一種常見用法是從字符串中提取出有意義的值,作為練習(xí),想一想如何實現(xiàn)
URLExtractor
以匹配代表 URL 的字符串。
在這本書的第一章中,我們學(xué)習(xí)了 Scala 模式匹配背后的提取器,學(xué)會了如何實現(xiàn)自己的提取器,及其在模式中的使用是如何和實現(xiàn)聯(lián)系在一起的。但是這并不是提取器的全部,下一章,將會學(xué)習(xí)如何實現(xiàn)可提取可變個數(shù)參數(shù)的提取器。
更多建議: