参考来源: https://windor.gitbooks.io/beginners-guide-to-scala/content https://docs.scala-lang.org/scala3/book
一个不同数据类型的模式匹配如下:
object Test {
def main(args: Array[String]) {
println(matchTest("two"))
println(matchTest("test"))
println(matchTest(1))
println(matchTest(6))
}
def matchTest(x: Any): Any = x match {
case 1 => "one"
case "two" => 2
case y: Int => "scala.Int"
case _ => "many" // Same as default in switch
}
}
case
的用法相当多,包括逻辑符号 |
以及 if
条件语句都可以适用,与 switch
的区别在于只会匹配一项,所以也不需要 break
case 0 | "" => false
case 2 | 4 | 6 | 8 | 10 => println("偶数")
case x if x == 2 || x == 3 => println("two's company, three's a crowd")
提取器 (Extractor Objects)
典型的提取器具有两个方法
object Twice {
def apply(x: Int): Int = x * 2
def unapply(z: Int): Option[Int] = if (z%2 == 0) Some(z/2) else None
}
-
unapply
最为关键,将被用于模式匹配x match { case Twice(n) => println(n) }
这里的
case Twice(n)
实际调用的就是Twice.unapply
- 如果只需要判断,
unapply
的返回值只要Boolean
就行,匹配代码也只需要case Twice()
,因为没有次值 (sub-value) - 如果需要返回的是单个次值,如这里例子所示,返回
Option[T]
- 如果需要返回多个次值,用
Option[Seq[B]]
- 如果只需要判断,
-
apply
并不必要,很多情况下只是模拟一个构造器,或者说是一个 工厂方法,尤其是对于样例类而言
提取器并不一定需要要在伴生对象中定义,匹配成功后的实例也可以通过 @
操作符来绑定到一个变量上
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)
}
以上是一个布尔提取器的例子
样例类 (Case Class)
样例类本质是个语法糖,可以看作是不可变的数据对象(
var
是允许的,但官方文档并不推荐),对所有构造参数增加getter
访问和toString
hashCode
equals
等方法 除了这些 “lombok” 功能外,最重要的是实现了一个伴生对象,并定义了apply
方法 和unapply
方法
样例类在编译阶段会混入 Product
特质 和 Serializable
特质,同时增加 copy
/ equals
/ hashCode
/ toString
等方法,这里的 copy
进行的是 浅拷贝; 伴生对象则增加了 apply
和 unapply
方法,继承了 AbstractFunction
(这其实是因为 Scala 中的工厂方法遵从 AbstractFunction
,而这个伴生对象也有工厂方法的成分)
当参数个数大于等于2,AbstractFunction
提供一个 tupled
方法作为参数来构造出结果
scala> case class A(x: Int, y:Int)
scala> A.tupled
res11: ((Int, Int)) => A = <function1>
scala> val t = (100,100)
t: (Int, Int) = (100,100)
scala> A.tupled(t)
res9: A = A(100,100)
样例类与模式识别
Scala 会自动为样例类创建一个 伴生对象 —— 包含了 apply
和 unapply
方法的 单例对象,因此可以直接被当作提取器使用
流提取器
中缀表达方式
列表、流 的解析和创建方法类似,都可以用 cons
操作符 ::
#::
,比如
val xs = 58 #:: 43 #:: 93 #:: Stream.empty
匹配也使用中缀方式,head #:: tail
也可以写成 #::(head, tail)
xs match {
case first #:: second #:: _ => first - second
case _ => -1
}
流提取器
完整的 #::
提取器代码如下
object #:: {
def unapply[A](xs: Stream[A]): Option[(A, Stream[A])] =
if (xs.isEmpty) None
else Some((xs.head, xs.tail))
}
如果流为空,提取器直接返回 None
,匹配直接失败。否则返回一个 Tuple2
,第一个元素是流的头,第二个元素是流的尾,尾本身当然也是一个流。这样,case head #:: tail
就会匹配有一个或多个元素的流。如果只有一个元素,则 tail
又会被绑定成空流。
将中缀写法换成一般写法,case first #:: second #:: _ => first - second
就成了 case #::(first, #::(second, _)) => first - second
。第一次调用时的 head
是 58,被绑给 first
,其余继续提取,second
被绑定成 43
,剩下的绑到 _
,也就直接扔掉。
序列提取
对于多个元素的列表可以对元素个数进行匹配:
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
}
要构造对应的提取器就需要用到 unapplySeq
来将某一对象解析成列表,且这一列表的长度在编译器并不确定
譬如,某个人可能名字很长,比如 Albus Percival Wulfric Brian Dumbledore
,我们现在希望通过提取器仅将姓氏和名字提出来,中间名单独分开来就行,写出来大致是这样的
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)))
}
}
用字符串操作把名字分割成列表,如果长度小于2则连姓名都无法提取到,返回 None
这里分割完后的是一个 Array
,直接通过 last
head
和 drop
操作就可以将姓名的三部分分开来,以构造 Some
的方式返回一个类型参数为 Tuple3
的 Option
值定义中的模式匹配
类似于 Python 中的 序列解包,Scala 在值定义过程中也可以使用模式,从而在赋值给变量的同时去结构它
case class Player(name: String, score: Int) // 每个玩家有姓名和得分
val Player(name, _) = currentPlayer() // 模式匹配
doSomethingWithName(name)
当然,如果无法匹配会抛 MatchError
,例如下面这个例子:
def scores: List[Int] = List()
val best :: rest = scores // 列表为空,匹配不到 best
println("The score of our champion is " + best)
值定义模式匹配可以很好地配合元组使用,从而提高代码可读性,如下面这个匹配方式:
def gameResult(): (String, Int) = ("Daniel", 3500)
如果我们用一个变量记录这一元组,会出现一个问题——Tuple
访问内部变量可读性很差:
val result = gameResult()
println(result._1 + ": " + result._2) // 有点回到Java Pair的意思
但是在赋值同时做解构就非常安全,且很整洁:
val result(name, score) = gameResult()
println(name + ": " + score)
循环中的模式匹配
结合 for {} yield
使用:
def gameResults(): Seq[(String, Int)] =
("Daniel", 3500) :: ("Melissa", 13000) :: ("John", 7000) :: Nil
def hallOfFame = for {
result <- gameResults()
(name, score) = result
if (score > 5000)
} yield name
可以看到,这个 result
实际也是多余,可以改成这样:
def hallOfFame = for {
(name, score) <- gameResults()
if (score > 5000)
} yield name
生成器左侧的模式也可以用来过滤,因为匹配失败的元素也会被直接滤掉 如下面这个滤掉所有空列表的方案:
val lists = List(1,2,3) :: List.empty :: List(5,3) :: Nil
for {
list @ head :: _ <- lists
} yield list.size
起到过滤效果的是这句 head :: _
,因为空列表无法匹配到 head
,因此只有非空列表能被匹配,从而绑定给 list
。
用模式匹配改造代码
匿名函数
对于一个 List
,可以很轻松地搭配匿名函数使用,如一个 List[String]
val songTitles = List("The White Hare", "Childe the Hunter", "Take no Rogues")
可以很轻松地使用 .map
搭配占位符 (placeholder) 和匿名函数:
songTitles.map(_.toLowerCase)
但是对于元组列表,模式匹配就很有用,考虑一个 (String, Int)
的元组列表 我们现在要对列表中第二个值高于3的对象,取出第一个对象组成新的列表
wordFrequencies.filter { case (_, f) => f > 3 && f < 25 } map { case (w, _) => w }
匿名函数的缺点在于,编译器无法从中推导出值的类型,必须显式声明
在上面这种方法中,虽然先 filter
再 map
解决了问题,但必须遍历两次,无论从效率还是代码整洁上看都不是最佳,因此可以用到偏函数 (PartialFunction)
偏函数
偏函数出现在集合 API 的 collect
方法,这个方法签名如下:
def collect[B](pf: PartialFunction[A, B]): Seq[B]
类型 PartialFunction[A, B]
扩展了类型 (A) => B
,是一个一元函数,主要包含 apply
和 isDefinedAt
两个方法,因此如果显示扩展一个 PartialFunction
特质,就要像这样实现这两个方法:
val pf = new PartialFunction[(String, Int), String] {
def apply(wordFrequency: (String, Int)) = wordFrequency match {
case (word, freq) if freq > 3 && freq < 25 => word
}
def isDefinedAt(wordFrequency: (String, Int)) = wordFrequency match {
case (word, freq) if freq > 3 && freq < 25 => true
case _ => false
}
}
可以看到,idSefinedAt
只用来过滤入参范围,而 apply
才是真正的业务逻辑,而 apply
也不能省略筛选部分代码,idDefinedAt
部分功能多少有些多余 我们完全可以用这样的方法定义偏函数:
val pf: PartialFunction[(String, Int), String] = {
cas (word, freq) if freq > 3 && freq < 25 => word
}
当然,偏函数只能用在 .collect
函数而不能用在 .map
函数中,因为这个函数是为处理一个集合中的所有元素设计的,有不匹配项会抛 MatchError
。