《Scala 程序设计》学习笔记 Chapter 4:模式匹配
- Scala 模式匹配支持获取对象状态;获取对象状态的操作往往称为“提取”或“解构”。[P86]
match
中的值、变量和类型
-
可以使用一个
Any
类的变量放到最后来充当default
。[P88] -
编译器会自动推断所有
case
子句返回值类型的最近公共父类型。[P88] -
在被匹配或提取的值中,编译器假定以大写字母开头的为类型名,以小写字母开头的为变量名。[P89]
-
注意一下模式匹配的变量含义与作用域:[P89]
def checkY(y: Int) = { for { x <- Seq(1, 2, 3) } { val str = x match { case y => "found y!" // 错误:并不是与变量 y 的值匹配,而是声明了一个 Any 类型的 y,这样会收到系统警告。 case i: Int => "int: " + i } println(str) } } checkY(1)
使用
``
包围变量以引用已经定义的变量。def checkY(y: Int) = { for { x <- Seq(1, 2, 3) } { val str = x match { case `y` => "found y!" // 正确 case i: Int => "int: " + i } println(str) } } checkY(1)
-
逻辑或语法:
case _: Int | _: Double => ...
序列的匹配
-
序列的基础语法 [P91]
- 使用
.empty[A]
构造空序列。 - 任意类型的空序列 用
Nil
表示。
- 使用
-
关于
Seq
的特殊语法:[P91 - 92]def seqToString[T](seq: Seq[T]): String = seq match { case head +: tail => s"$head +: " + seqToString(tail) // 1 case Nil => "Nil" }
-
+:
是“构造” 操作符,以:
结尾的方法向右结合,即向Seq
的尾部结合。head
和tail
是Seq
自带的两个方法,但在这里,按照惯例被解释为一个变量,作用是提取一个非空序列的头部(第一个元素)和尾部(除了第一个元素外的其他元素)。
-
-
Map
不是Seq
的子类型,要通过map.toSeq
生成seq
。[P92]
case
中的 guard 语句 [P95]
for ( i <- Seq(1, 2, 3, 4)) {
i match {
case _ if i % 2 == 0 => println(s"even: $i")
case _ => println(s"odd: $i")
}
}
case 类的匹配
-
使用
Seq.zipWithIndex
方法将一个Seq
连同序号一起打印出来:[P95 -96]val itemsCosts = Seq(("Pencil", 0.52), ("Paper", 1.35)) val itemsCostsIndices = itemsCosts.zipWithIndex for (itemCostIndex <- itemsCostsIndices) { itemCostIndex match { case ((item, cost), index) => println(s"$index: $item costs $cost each") } }
调用
zipWithIndex
返回的元组形式为((name, cost), index)
。
unapply
方法
-
unapply
方法用于提取和解构。当在match
中对 case 类使用诸如case Person("Alice", 25, Address(_, "Chicago", _)) => ...
之类的语法时,会调用其unapply
方法。(当然在本例中Address
也需要unapply
方法)[P96] -
unapply
方法的一种定义:[P96 - 97]def unapply(P: Person): Option[Tuple3[String, Int, Address]] = Some((p.name, p.age, p.address))
-
用
Option
的原因是,unapply
方法可以选择“否决”这个请求,返回None
详见unapplySeq
。 -
从 Scala 2.11.1 开始,
unapply
方法可以返回任意类型�,只要该类型具有以下方法:def isEmpty: Boolean def get: T
-
-
有必要时,
unapply
会被递归调用(比如本例中的Address
)。[P97] -
元组字面量语法:[P97 -> P50]
val t1 = Option[Tuple3[String, Int, Address]] = ... val t2 = Option[(String, Int, Address)] = ... val t3 = Option[ (String, Int, Address) ] = ... // 更容易阅读
-
让
unapply
支持任意非空集合:使用:+
[P97 - 98]-
:+
是一个单例对象,它的unapply
使用以下语法:def unapply[T, Coll](collection: Coll): Option[(T, Coll)]
但是这样的
unapply
使用的调用方法为:case +: (head, tail) => ...
可以写成如下形式:def processSeq2[T](l: Seq[T]): Unit = l match { case +: (head, tail) => println("%s +: ", head) processSeq2(tail) case Nil => println("Nil") }
当然,也可以使用
head +: tail
,这是编译器提供的语法糖。同样的语法糖还有:case class With[�A, B](a: A, b: B) val with1: With[Stirng, Int] = With("Foo", 1) val with2: With[String, Int] = With("Bar", 2) Seq(with1, with2) foreach { w match { case s With i => println(s"$s with $i) case _ => println(s"Unknown $w") } }
但是,同样的语法不能用于初始化。
-
使用
:+
逆序处理一个序列。[P99]def reverseSeqToString[T](l: Seq[T]): String = l match { case prefix :+ end => reverseSeqToString(prefix) + s" :+ $end" case Nil => "Nil" }
-
-
补充:对于
List
,:+
/+:
需要 O(n) 的时间复杂度,对于Vector
之类的其他某些序列,只需要 O(1) 的时间复杂度。[P99]
unapplySeq
方法
-
除了
apply
方法外,Seq
的伴随对象还实现了unapplySeq
方法:[P100]def apply[A](elems: A*): Seq[A] def unapplySeq[A](x: Seq[A]): Some[Seq[A]]
在
case
中,使用如下语法调用unapplySeq
:def windows[T](seq: Seq[T]): String = seq match { case Seq(head1, head2, _*) => s"($head1, $head2), " +����� windows(seq.tail) case ... ... }
-
当然也可以使用
+:
语法:[P101]def windows2[T](seq: Seq[T]): String = seq match { case head1 +: head2 +: tail => s"($head1, $head2), " +����� windows2(seq.tail) case ... ... }
-
Seq
的sliding
方法:[P101]- 返回一个“惰性”迭代器。对这个迭代器调用
toSeq
方法,可以将迭代器转为一个collection.immutable.Stream
(一个惰性列表,创建时即对列表的头部元素求值,但只在需要的时候才会对列表的尾部元素求值。toList
会在创建时对所有元素求值)。
- 返回一个“惰性”迭代器。对这个迭代器调用
可变参数列表的匹配
-
使用
name @ _*
匹配可变参数:[P102]case WhereIn(col, val1, vals @ _*) => ...
正则表达式匹配
-
使用
.r
方法生成正则表达式。[P103]val BookExtractorRE = """Book: title=([^,]+),\s+author=(.+)""".r
在
match
中使用case BookExtractorRE(title, author) => ...
-
使用三重引号表示正则表达式字符串的原因是可以不用对正则中的
\
等符号单独进行转义。[P103] -
在三重引号内的正则表达式中使用变量插值是无效的,如果使用了变量插值,就需要对
\
等符号进行转义操作。[P103]
再谈 case
语句的变量绑定
-
name @ object
语法:[P104]person match { case p @ Person("Alice", 25, address) => ... case p @ Person("Bob", 29, a @ Addres(street, city, country)) => ... }
p @ ...
的语法将整个Person
类的实例赋值给了变量p
。如果不需要从Person
实例中提取属性值,只要写为p: Person => ...
就可以了。
再谈类型匹配
-
JVM 类型擦除:为了避免与旧版本代码断代,JVM 的字节码不会记住一个泛型实例(如
List
)中实际传入的类型与参数信息。所以在match
中,不能区分Seq[String]
与Seq[Double]
,要自定义匹配函数:x match { case seq: Seq[_] => (s"seq ${doSeqMatch(seq)}", seq) case _ => ("Unknown!", x) } def doSeqMatch[T](seq: Seq[T]): String = seq match { case Nil => "Noting" case head +: _ => head match { case _: Double => "Double" case _: String => "String" case _ => "Unmatched seq element" } }
封闭继承层级与全覆盖匹配
-
如果类型的继承层级可能发生变化,就应当避免使用
sealed
。[P107] -
在父类型中,不带参数的抽象方法可以再子类中用
val
变量实现。推荐的做法是:在抽象父类型中声明一个不带参数的抽象方法,这样就给子类型如何具体实现该方法留下了巨大的自由,既可以用方法实现,又可以用val
变量实现。[P107]sealed abstract class HttpMethod() { def body: String def bodyLength: body.length }
-
编译器无法判断
Enumeration
相应的match
语句是否全覆盖。[P107]
模式匹配的其他用法
-
定义变量:
val Person(name, age, Address(_, state, _)) = Person("Dean", 29, Address("1 Scala Way", "CA", "USA)) // 得到 name, age, state; val head +: tail = List(1, 2, 3) // head: Int = 1 // tail: List[Int] = List(2, 3) val Seq(a, b, c) = List(1, 2, 3) // 得到 a, b, c val Seq(a, b, c) = List(1, 2, 3, 4) // MatchError
-
在
if
中也可以使用模式匹配,但不能用_
占位符。[P108]val p = Person("Dean", 29, Address("1 Scala Way", "CA", "USA")) if (p == Person("Dean", 29, Address("1 Scala Way", "CA", "USA"))) "yes" else "no" // "yes"
-
Scala 对一些非字母数字的字符做了”字符映射“,使得他们符合 JVM 规范。比如:
=
会被映射为$eq
。[�P108] -
元组:[P109]
def sum_count(ints: Seq[Int]) = (ints.sum, ints.size) val (sum, count) = sum_count(List(1, 2, 3, 4, 5)) // sum: Int = 15 // count: Int = 5
-
在带复杂参数的函数字面量中使用:[P109 - 110]
case class Address(street: String, city: String, country: String) case class Person(name: String, age: Int) val as = Seq( Address("1 Scala Lane", "Anytown", "USA"), Address("2 Clojure Lane", "Othertown", "USA")) val ps = Seq( Person("Buck Trends", 29) Person("Clo Jure", 28) ) val pas = ps zip as // Seq[(Person, Address)] pas map { case (Person(name, age), Address(street, city, country)) => s"$name (age: $age) lives at $street, $city, in $country" }
-
在正则表达式中使用模式匹配去解构字符串:[P110]
val cols = """\*|[\w, ]+""" val table = """\w+""" val tail = """.*""" val selectRE = s"""SELECT\\s*(DISTINCT)?\\s+($cols)\\s*FROM\\s+($table)\\s*($tail)?;""".r val selectRE(distinct1, cols1, table1, otherClauses) = "SELECT DISTINCT * FROM atable;" /* distinct1: String = DISTINCT cols1: String = * table1: String = atable otherClauses: String = "" */ val selectRE(distinct2, cols2, table2, otherClauses) = "SELECT col1, col2 FROM atable;" /* distinct1: String = null cols1: String = "col1, col2" table1: String = atable otherClauses: String = "" */
由于使用了变量插值,在正则表达式字符串中必须增加
\
转义。 -
要谨慎对待默认
case
子句:什么情况下才应该出现“以上均不匹配”。[P111]