Scala新手指南中文版 - 第三篇 Patterns Everywhere(模式无处不在)

译者注:原文出处http://danielwestheide.com/blog/2012/12/05/the-neophytes-guide-to-scala-part-3-patterns-everywhere.html,翻译:Thomas

 

在前两篇中,我花了不少时间解释了case class在模式中如何解构的,如何实现能够以任何方式提取任何类型数据的提取器。

到目前为止你只看到少数使用模式的方法,现在是时候来看看在你的Scala代码里该如何用上模式,我们开始吧!

 

模式匹配表达式

模式时常会出现的场景是在模式匹配表达式里,在参加了Coursera的Scala课程或者完整看了前两篇文章后,你应该对这种用法很熟悉了。首先有个表达式e,后面跟着match关键字和一个代码块,代码块里可以包含任意多个case子句。一个case子句由case关键字开头 后面跟着模式和一个可选的防护从句,后面跟随着模式匹配成功后要执行的一段代码。

下面是一个使用了模式和一个包含了防护从句的case的简单的例子:

 

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语句一样。在这里称作模式匹配表达式是有原因的,整个match的返回值是第一个被匹配的模式的返回值。

 

通常,用上它的这个特性是一个好办法,这让你可以解耦两个原本不属于一样的事情,也让你的代码更容易测试。我们可以将上面的代码重写一下:

 

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方法。现在它是一个纯函数了,返回的是模式匹配的结果。你当然可以把结果赋给一个常量。

 

在常量/变量定义时使用模式

在Scala中还可以把模式用在常量定义语句的左侧(变量定义里当然也可以用,不过我们还是想尽量保持Scala代码的函数式风格,所以你在我的系列文章里不会看到太多的变量的使用)。假设我们有一个方法会返回当前玩家,我们先实现一个假的方式总是返回一个固定的玩家:

 

def currentPlayer(): Player = Player("Daniel", 3500)
通常你的常量定义看起来是这样的:

 

 

val player = currentPlayer()
doSomethingWithTheName(player.name)
如果你是个Python程序员,也许对序列解包功能很熟悉。Scala提供类似的功能,你可以在常量定义语句的左侧使用任何模式,我们来将上面的代码改造一下,让它可以结构player后再赋值给指定常量:

 

 

val Player(name, _) = currentPlayer()
doSomethingWithTheName(name)
你可以拿任意模式用作此用途,不过通常你得要确保模式总是被匹配的,否则你就会面临运行时异常。例如,下面的例子就会有问题。 scores是一个返回得分列表的方法,在下面的代码中,这方法如果返回的是空列表就会导致问题:

 

 

def scores: List[Int] = List()
val best :: rest = scores
println("The score of our champion is " + best)
看,程序丢出来一个MatchError。结果就是我们的游戏程序跑不下去了,因为没有获取到游戏成绩。

 

想要这样来使用模式,最好的方法是结构case class,你可以在编译时就能确定它们的类型。如果同时还使用了tuple的话,可以让你的代码可读性更好。假设我们有一个函数返回一个tuple包装的玩家的名字和他的得分,不再使用Player类了:

 

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

 

访问tuple的元素看上去会让人感觉迷惑:

 

val result = gameResult()
println(result._1 + ": " + result._2)
这时在常量定义时用模式来结构会是安全的,因为我们清楚我们面对的是一个Tuple2数据类型:

 

 

val (name, score) = gameResult()
println(name + ": " + score)
这比前一段代码更可读,不是吗?

 

 

在for语句中使用模式

在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

 

hallOfFame的结果会是List("Melissa", "John"), 因为第一个玩家没有满足守卫子句的条件。

我们还可以进一步精简一下代码,在for语句里,generator的左侧也可以是模式。所以我们可以不用引进result常量,可以直接在generator左侧用模式来解构:

 

def hallOfFame = for {
   (name, score) <- gameResults()
  if (score > 5000)
} yield name
在这里例子里,模式(name,score)总是匹配的,所以在没有守卫子句 if (score > 5000) 时,这段代码就相当于把tuple转成玩家名字 。

 

还有一点很重要,在generator的左侧的模式还兼具过滤功能 - 如果左侧模式不匹配,相应的元素就会被过滤掉。举例来说明,假设我们有一个列表的列表,我们想要返回所有非空列表的大小。这意味着我们必须过滤掉空list,然后返回留下的列表的大小。下面是一种解决方式:

val lists = List(1, 2, 3) :: List.empty :: List(5, 3) :: Nil

for {
  list @ head :: _ <- lists
} yield list.size

generator左侧的模式不匹配空list。它不会抛出MatchError,而是会过滤掉空list,所以结果就会返回List(3,2)。在for语句中使用模式是一种非常自然和强大的组合,使用一段Scala后,你会爱上它们的。

匿名函数

最后,模型还可以用来定义匿名函数。如果你在Scala里用过catch语句块来处理异常,那么其实你已经用过这种用法了。模式匹配匿名函数是一个需要单独篇章来讲解的题目,因为有太多东西要说了,所以我就不会在这个关于模式的篇章里涉及太多,而会在本系列的后篇来介绍。

更新:修复一个hallOfFame 中的错误。感谢Rajiv帮忙找到错误.

作者:Daniel Westheide,2012.12.5

 

 

 

你可能感兴趣的:(scala)