Simple Scala: Pattern in Everywhere

简介

在提取器和序列提取中详细介绍了:

1. 用模式结构对象是怎么一回事
2. 如何构造自己的提取器

这里介绍模式更多的用法.

模式匹配表达式

模式可能出现的一个地方是 “模式匹配表达式”,一个表达式e,后面跟着关键字match以及一个代码块,这个代码块包含了一些匹配样例; 而每个样例又包含了case关键字,模式,可选的守卫分句,以及最右边的代码块;如果匹配成功,最右边的代码块就会执行.写成代码,看起来会是这个样子:

e match {
  case Pattern1 => block1
  case Pattern2 if-clause => block2
  ...
}

下面是一个更具体的例子:

case class Player(name: String, score: Int)
def printMessage(player: Player) = player match {
  case Player(_, score) if score > 100000 =>
    println("Get a job, dude!")
  case Player(name, _) =>
    println("Hey, $name, nice to see you again!")
}

printMessage的返回类型为Unit,其唯一目的是执行一个副作用,即打印一个信息.记住你不一定非要使用模式匹配,因为你也可以使用像java中的switch语句.

但这里使用的模式匹配表达式,之所以叫做”模式匹配表达式”,是因为其返回值是由第一个匹配的模式中的代码块决定的.

使用它通常是好的,因为它解耦两个并不真正属于彼此的东西,也使得代码更易于测试.可以把上面的例子重写成下面这样:

case class Player(name: String, score: Int)
def message(player: Player) = player match {
  case Player(_, score) if score > 100000 =>
    "Get a job, dude!"
  case Player(name, _) =>
    "Hey, $name, nice to see you again!"
}
def printMessage(player: Player) = println(message(player))

现在,独立出一个返回值类型是String得message函数,它是一个纯函数,没有任何副作用,返回模式匹配表达式的结果,你可以将其保存为值,或者赋给一个变量.

值定义中的模式

模式还能出现值定义的左边.

假设有一个方法,返回当前的球员,我们可以模拟这个方法,让他始终返回同一个球员:

def currentPlayer(): Player = Player("Daniel", 3500)

通常的值定义如下所示:

val player = currentPlayer()
doSomethingWithName(player.name)

如果你知道python,你一定了解一个叫做序列解包的功能,它允许在值定义的左侧使用模式.你可以用类似的风格编写Scala代码,改变上面的代码,在将球员赋值给左侧变量的同时去结构它:

val Player(name, _) = currentPlayer()
doSomethingWithName(name)

你可以用任何模式来做这件事情,但得确保模式总能够匹配,否则代码会在运行时出错.下面的代码就是有问题的:scores方法返回球员得分的列表,为了说明问题,代码中只是返回一个空得列表:

def scores: List[Int] = List()
val best :: rest = scores
println("The score of our champion is " + best)

运行的时候就会出现MatchError.

一种安全且非常方便的方式是只结构那些在编译期就知道类型的样例类.此外,以这种方式使用元组,代码可读性更强.假设有一个函数,返回一个包括球员名字及其得分的元组,而不是先前定义的Player:

def gameResult(): (String, Int) = ("Daniel", 3500)

访问元组字段的代码给人感觉总是很怪异:

val result = gameResult()
println(result._1 + ": " + result._2)

这样,在赋值的同时结构它是非常安全的,因为我们知道他的类型为Tuple2:

val (name, score) = gameResult()
println(name + ": " + score)

for语句中的模式

模式在for语句中也非常重要.所有能在值定义的左侧使用的模式都适用于for语句的值定义.因此,如果我们有一个球员得分集,想进入谁能进入名人堂,用for语句就可以解决:

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

结果为List(“Melissa”, “John”),因为第一个球员得分没有超过5000.
上面的代码可以写的更简单,for语句中,生成器的左侧也可以是模式.从而,可以直接在左侧把想要的值结构出来:

def hallOfFame = for {
    (name, score) <- gameResults()
    if (score > 5000)
} yield name

模式(name,score)总会匹配成功,如果没有守卫语句if (score > 5000),for语句就相当于直接将元祖映射到球员名字,不会进行过滤.

不过你要知道,生成器的左侧也可以用来过滤,如果左侧的模式匹配失败,那相关的元素就会被直接过滤掉.

为了说明这种情况,假设有一序列的序列,我们想返回所有非空序列的元素个数.这就需要过滤掉所有的空列表,然后再返回剩下列表的元素个数,下面是一个解决方案:

val lists = List(1, 2, 3) :: List.empty :: List(5, 3) :: Nil
for {
  list @ head :: _ <- lists
} yield list.size

上面的例子中,左侧的模式不匹配空列表,这不会抛出MatchError,但对应的空列表会被丢弃掉,因此得到的结果是List(3,2).

模式和for语句是一个很自然很强大的结合,在以后的应用中会经常使用.