简介
模式匹配中经常结构各种数据结构,包括列表/流/样例类等,这种结构数据结构功能的实现,都要归功于提取器.
提取器使用很广泛,具有与构造器相反的效果:构造器从给定的参数列表创建一个对象,而提取器却是从传递给他的对象中提取出构造该对象的参数.同时Scala标准库中包含了一些预定义的提取器.
样例类比较特殊,Scala会为其创建一个半生对象:一个包含了apply和unapply方法的单例对象.apply用来创建样例类的实例,unapply需要被半生对象实现,以使其成为提取器.
第一个提取器
unapply方法可能不止有一种方法签名,不过,我们还是从最简单的开始,毕竟使用更广泛的还是只有一种方法签名的unapply.假设要创建一个User特质,还有两个类包含他,并带有一个字段:
trait User {
def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User
我们想再各自的伴生对象中实现FreeUser和PremiumUser类的提取器,就像Scala为样例类做得一样.如果想让样例类只支持从给定对象中提取单个参数,那unapply的方法签名看起来像这个样子:
def unapply(object: S): Option[T]
这个方法接受一个类型为S的对象,返回类型T的Option,T就是要提取的参数类型.
下面实现为两个类实现提取器:
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)
}
现在就可以在REPL中调用他们了:
scala> FreeUser.unapply(new FreeUser("Daniel"))
res0: Option[String] = Some(Daniel)
如果调用返回的结果是 Some[T],说明提取模式匹配成功,如果是None,说明模式不匹配.
一般不会直接调用它,因为用于提取器模式时,Scala会隐式的调用提取器的apply方法:
val user: User = new PremiumUser("Daniel")
user match {
case FreeUser(name) => "Hello" + name
case PremiumUser(name) => "Welcome back, dear" + name
}
你会发现,两个提取器绝对不会返回None,这个例子展示的提取器比之前更有意义,如果你有一个类型不确定的对象,你可以同时检查其类型并解构.
上面的例子中,FreeUser并不会匹配,因为它接受的类型跟我们传递给他得不一样,这样,user对象就会被传递给第二个模式,也就是PremiumUser半生对象的unapply方法,而这个模式就会匹配成功,从而返回值就被绑定在name上了.
提取多个值
现在,假设类有多个字段:
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
如果想要结构出多个参数,那它的unapply方法就应该有这样的方法签名:
def unapply(object: S): Option[(T1, ..., T2)]
这个方法接受S类型的对象,返回类型参数为TupleN的Option实例,TupleN中的N是要提取的参数个数.
修改类之后,提取器也要做对应的修改:
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))
}
现在就可以拿来做模式匹配了:
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"
}
布尔提取器
有些时候,进行模式匹配并不是为了提取参数,而是为了检查是否匹配.这种情况下,第三种unapply方法签名就有用了.这个方法接受一个S类型的对象,返回一个布尔值:
def unapply(object: S): Boolean
使用的时候,如果这个提取器返回true,模式会匹配成功,否则,Scala会尝试那Object匹配下一个模式.
上个例子存在一些代码逻辑,用来检查一个免费用户有没有可能被说服去升级他得账户.可以把这个逻辑放在一个单独的提取器中:
object premiumCandidate {
def unapply(user: FreeUser): Boolean = user.upgradeProbability > 0.75
}
你会发现,提取器不一定要在这个类的半生对象中定义.正如其定义一样,这个提取器的使用方法也很简单:
val user: User = new FreeUser("Daniel", 2500, 0.8d)
user match {
case freeUser @ premiumCandidate() => initiateSpamProgram(freeUser)
case _ => sendRegularNewsletter(user)
}
使用的时候,只需要把一个空的参数列表传递给提取器,因为它并不是真的需要提取数据,自然也就没有必要绑定变量.
这个例子有一个看起来比较奇怪的地方: 我假设存在一个空想的 initiateSpamProgram 函数,其接受一个 FreeUser 对象作为参数。 模式可以与任何一种 User 类型的实例进行匹配,但 initiateSpamProgram 不行, 只有将实例强制转换为 FreeUser 类型, initiateSpamProgram 才能接受。
因为如此,Scala 的模式匹配也允许将提取器匹配成功的实例绑定到一个变量上, 这个变量有着与提取器所接受的对象相同的类型。这通过 @ 操作符实现。 premiumCandidate 接受 FreeUser 对象,因此,变量 freeUser 的类型也就是 FreeUser 。
布尔提取器的使用并没有那么频繁(就我自己的情况来说),但知道它存在也是很好的, 或迟或早,你会遇到一个使用布尔提取器的场景.
中缀表达方式
解构列表、流的方法与创建它们的方法类似,都是使用 cons 操作符: :: 、 #:: ,比如:
val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
case first #:: second #:: _ => first - second
case _ => -1
}
你可能会对这种做法产生困惑。 除了我们已经见过的提取器用法,Scala 还允许以中缀方式来使用提取器。 所以,我们可以写成 e(p1, p2) ,也可以写成 p1 e p2 , 其中 e 是提取器, p1 、 p2 是要提取的参数。
同样,中缀操作方式的 head #:: tail 可以被写成 #::(head, tail) , 提取器 PremiumUser 可以这样使用: name PremiumUser score 。 当然,这样做并没有什么实践意义。 一般来说,只有当一个提取器看起来真的像操作符,才推荐以中缀操作方式来使用它。 所以,列表和流的 cons 操作符一般使用中缀表达,而 PreimumUser 则不用。
进一步看流提取器
尽管 #:: 提取器在模式匹配中的使用并没有什么特殊的, 但是,为了更好的理解上面的代码,还是进一步来分析一下。 而且,这是一个很好的例子,根据要匹配的数据结构的状态,提取器很可能返回 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 就会被绑定成空流。
为了理解流提取器是怎么在模式匹配中工作的,重写上面的例子,把它从中缀写法转成普通的提取器模式写法:
val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
case #::(first, #::(second, _)) => first - second
case _ => -1
}
首先为传递给模式匹配的初始流 xs 调用提取器。 由于提取器返回 Some(xs.head, xs.tail) ,从而 first 会绑定成 58, xs 的尾会继续传递给提取器,提取器再一次被调用,返回首和尾, second 就被绑定成 43 , 而尾就绑定到通配符 _ ,被直接扔掉了。
序列提取
对于某种数据结构来说,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!"
}