1.序列的匹配
Seq是具体的集合类型的父类型,这些集合类型支持以确定顺序遍历其元素,如List和Vector
我们来考察用模式匹配和递归方法遍历Seq 的传统方法,顺便学习一些关于序列的基础知识。
val nonEmptySeq = Seq(1, 2, 3, 4, 5) // 构造一个非空的Seq[Int](事实上返回了一个List);然后用惯用方法构造了一个空的Seq[Int]。 val emptySeq = Seq.empty[Int] val nonEmptyList = List(1, 2, 3, 4, 5) // 构造一个非空的List[Int](Seq 的一个子类型);然后用Scala 库的一个专用对象Nil,表示任意类型的空List。 val emptyList = Nil val nonEmptyVector = Vector(1, 2, 3, 4, 5) // 构造一个非空的Vector[Int](Seq 的一个子类型);然后构造了一个空的Vector[Int]。 val emptyVector = Vector.empty[Int] val nonEmptyMap = Map("one" -> 1, "two" -> 2, "three" -> 3) // 构造了一个非空的Map[String,Int],这不是Seq 的子类型。在接下来的讨论中我们会涉及这一点。Map[String,Int] 的键为String 类型,值为Int 类型。然后构造了一个空的Map[String,Int]。 val emptyMap = Map.empty[String,Int] def seqToString[T](seq: Seq[T]): String = seq match { // 定义了一个递归方法,从Seq[T] 中构造String,T 为某种待定的类型。方法体是用来与输入的Seq[T] 相匹配。 case head +: tail => s"$head +: " + seqToString(tail) // 这里存在两个互斥的match 子句。第一个子句匹配非空的Seq,提取其头部(第一个元素)以及尾部(除头部以外,剩下的元素)。(Seq 有head 和tail 方法,但在这里,这两个标识符按case 子句的惯例被解释为变量。)在case 子句中,用提取的头部加上“+:”,以及尾部的字符串表示来构造一个字符串。尾部的字符串表示由调用seqToString 产生。 case Nil => "Nil" // 另外一个case 只可能是空Seq。我们用表示空List 专用的对象Nil 来匹配。注意, 任何Seq 的尾部都可以认为是以一个相应类型的空Seq,事实上,List 就是这么实现的。 } for (seq <- Seq( // 将以上这些Seq 作为元素放到另一个大Seq 中(对其中的Map 调用toSeq,将其转为键-值对组成的序列),然后遍历该序列,并打印各个序列调用seqToString 返回的结果。 nonEmptySeq, emptySeq, nonEmptyList, emptyList, nonEmptyVector, emptyVector, nonEmptyMap.toSeq, emptyMap.toSeq)) { println(seqToString(seq)) }
以下为执行结果:
1 +: 2 +: 3 +: 4 +: 5 +: Nil Nil 1 +: 2 +: 3 +: 4 +: 5 +: Nil Nil 1 +: 2 +: 3 +: 4 +: 5 +: Nil Nil (one,1) +: (two,2) +: (three,3) +: Nil Nil
Map 并不是Seq 的子类型,因为Map 不保证遍历的顺序是固定的。因此,我们调用Map.toSeq 创建一个键- 值元组的序列。由此得到的Seq 键值对(pair)将会按照插入的顺序进行遍历。这种副作用仅限于小的Map 的实现上,并不是针对所有Map 的一般性保证。这里的空集合表明seqToString 方法对空集也能正常工作。
这里有两种新的case 子句。第一个子句中,head +: tail 匹配了序列的头部和尾部。+:操作符是序列的“构造”操作符,与我们在优先规则中为List 使用的:: 操作符类似。回想一下,以冒号(:) 结尾的方法向右结合,即向Seq 的尾部结合。
虽然它们被称作“操作符”和“方法”,但其实并不太准确;我们会在之后的章节讨论这些表达式,现在先来关注几个关键点。
首先,case 子句只匹配至少包含一个头部元素的非空序列,它将序列的头部和剩下的部分分别提取到可变变量head 和tail 中。
第二,重申一下,这里的head 和tail 是任意的两个变量名。然而,Seq 类型也分别存在两个名为head 和tail 的方法,用于提取序列的头部和尾部元素。通常情况下,我们可以从上下文中清晰地看出这两个标识符是函数还是变量。顺便提一下,对空序列调用这两个方法时,编译器会抛出异常。
Seq 的行为很符合链表的定义,因为在链表中,每个头结点除了含有自身的值以外,还指向链表的尾部(即链表剩下的元素),从而创建了一种层级结构,类似以下四个节点所组成的的序列。在这里尾部添加了一个空序列:
(node1, (node2, (node3, (node4, (end))))
Scala 库中有一个名为Nil 的对象,可以匹配所有的空序列。我们甚至可以用Nil 来表示非List 的其他空集合,因为序列对相等操作的实现都是一样的,不必精确匹配具体类型。
以下是上述程序的一个变体,增加了括号,这次我们只使用了几个集合类型:
val nonEmptySeq = Seq(1, 2, 3, 4, 5) val emptySeq = Seq.empty[Int] val nonEmptyMap = Map("one" -> 1, "two" -> 2, "three" -> 3) def seqToString2[T](seq: Seq[T]): String = seq match { case head +: tail => s"($head +: ${seqToString2(tail)})" // 重新格式化字符串,增加了外边的括号,(…)。 case Nil => "(Nil)" } for (seq <- Seq(nonEmptySeq, emptySeq, nonEmptyMap.toSeq)) { println(seqToString2(seq)) }
如下所示,脚本输出清楚地显示了层级结构,每个“子列表”都被括号包围:
(1 +: (2 +: (3 +: (4 +: (5 +: (Nil)))))) (Nil) ((one,1) +: ((two,2) +: ((three,3) +: (Nil))))
我们只用了两个case 子句和递归就处理了序列。这暗示了所有序列的基础特性:序列要么为空,要么非空。这听起来很老套,但一旦理解了这一点,你就会多一个基于“分治”法的工具。processSeq 就多次使用该方法。
2. 元组的匹配
通过元组字面量,很容易对元组进行匹配
val langs = Seq( ("Scala", "Martin", "Odersky"), ("Clojure", "Rich", "Hickey"), ("Lisp", "John", "McCarthy")) for (tuple <- langs) { tuple match { case ("Scala", _, _) => println("Found Scala") // 匹配一个三元组的元组,其中第一个元素为字符串Scala,忽略第二和第三个元素。 case (lang, first, last) => // 匹配任意三元素元组,其中的元素可以为任意类型,但在这里,由于输入的值为lang,元素类型被推断为String。元组的三个元素被提取到变量lang、first 和last 中。 println(s"Found other language: $lang ($first, $last)") } }
该示例的输出如下:
Found Scala Found other language: Clojure (Rich, Hickey) Found other language: Lisp (John, McCarthy)
元素可以拆分为各个组成的元素。我们可以匹配元组中任意位置的字面量,同时忽略我们不需要的值。
3.case 类的匹配
我们现在来看更多关于深度匹配的例子,并对case 类对象的内容进行考察:
case class Address(street: String, city: String, country: String) case class Person(name: String, age: Int, address: Address) val alice = Person("Alice", 25, Address("1 Scala Lane", "Chicago", "USA")) val bob = Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA")) val charlie = Person("Charlie", 32, Address("3 Python Ct.", "Boston", "USA")) for (person <- Seq(alice, bob, charlie)) { person match { case Person("Alice", 25, Address(_, "Chicago", _) => println("Hi Alice!") case Person("Bob", 29, Address("2 Java Ave.", "Miami", "USA")) => println("Hi Bob!") case Person(name, age, _) => println(s"Who are you, $age year-old person named $name?") } }
输出如下:
Hi Alice! Hi Bob! Who are you, 32 year-old person named Charlie?
我们可以匹配嵌套类型的内容。以下的例子使用的元组更接近真实生活。想象一下,我们有一个(String,Double) 元组组成的序列,表示商店里的商品名称与商品价格,同时想要将它们连同序号一起打印出来。为解决上面的问题,我们可以用到Seq.zipWithIndex 方法:
val itemsCosts = Seq(("Pencil", 0.52), ("Paper", 1.35), ("Notebook", 2.43)) val itemsCostsIndices = itemsCosts.zipWithIndex for (itemCostIndex <- itemsCostsIndices) { itemCostIndex match { case ((item, cost), index) => println(s"$index: $item costs $cost each") } }
我们在REPL 里运行以上脚本,用:load 命令观察变量的类型和运行的输出(略加整理了一下格式):
Loading src/main/scala/progscala2/patternmatching/match-deep-tuple.sc... itemsCosts: Seq[(String, Double)] = List((Pencil,0.52), (Paper,1.35), (Notebook,2.43)) itemsCostsIndices: Seq[((String, Double), Int)] = List(((Pencil,0.52),0), ((Paper,1.35),1), ((Notebook,2.43),2)) 0: Pencil costs 0.52 each 1: Paper costs 1.35 each 2: Notebook costs 2.43 each
调用zipWithIndex 时,返回的元组形式为((name,cost),index)。通过匹配这种形式,我们提取了输入元组的三个元素,并将其打印出来。这样的代码是我经常使用的。