Scala match case 集合序列 元组和类的匹配

2019-05-04 18:09:45 | 编辑 | 添加

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)。通过匹配这种形式,我们提取了输入元组的三个元素,并将其打印出来。这样的代码是我经常使用的。