【Scala之旅】样例类与模式匹配

萧然 2019-06-27

本节翻译自

综述:模式匹配是一个十分强大的机制,可以应用在很多场合:switch 语句、类型查询,以及“析构”(获取复杂表达式中的不同部分)。样例类针对模式匹配进行了优化。

样例类

样例类就像普通的类一样,但有一些关键的区别,我们将会在下面对它们进行讨论。样例类对建模不可变数据很有帮助。在接下来的步骤中,我们将会看到它们在模式匹配中的重要作用。

定义样例类

一个最小的样例类需要关键字 case class、一个标识符和一个参数列表(列表可为空):

case class Book(isbn: String)

val frankenstein = Book("978-0486282114")

注意,不需要用关键字 new 来实现 Book 样例类;这是因为样例类有一个默认的 apply 方法,它负责对象的构造。

当您创建带有参数的样例类时,参数是公共的 val

case class Message(sender: String, recipient: String, body: String)
val message1 = Message("[email protected]", "[email protected]", "Ça va ?")

println(message1.sender)  // prints [email protected]
message1.sender = "[email protected]"  // this line does not compile

你不能重新指定 message1.sender,因为它是 val(即不可变的)。样例类允许创建 var,但并不鼓励这样做。

比较

用结构来比较样例类,而不是引用:

case class Message(sender: String, recipient: String, body: String)

val message2 = Message("[email protected]", "[email protected]", "Com va?")
val message3 = Message("[email protected]", "[email protected]", "Com va?")
val messagesAreTheSame = message2 == message3  // true

尽管 message2message3 引用不同的对象,但每个对象的值是相等的。

复制

你可以通过使用 copy 方法创建一个样例类实例的一个(浅)副本。你还可以选择性地更改构造函数参数。

case class Message(sender: String, recipient: String, body: String)
val message4 = Message("[email protected]", "[email protected]", "Me zo o komz gant ma amezeg")
val message5 = message4.copy(sender = message4.recipient, recipient = "[email protected]")
message5.sender  // [email protected]
message5.recipient // [email protected]
message5.body  // "Me zo o komz gant ma amezeg"

message4 的接收者 recipient,被用作 message5 的发送者 sender,但是 message4body 被直接复制。

模式匹配

模式匹配是一种根据模式检查值的机制。一个成功的匹配也可以将一个值分解为它的组成部分。它是Java中 switch 语句的一个更强大的版本,它也可以用来代替一系列 if/else 语句。

语法

一个匹配表达式有一个值、关键字 match 和至少一个 case 子句。

import scala.util.Random

val x: Int = Random.nextInt(10)

x match {
  case 0 => "zero"
  case 1 => "one"
  case 2 => "two"
  case _ => "many"
}

上面的 val x 是一个介于 0 到 10 之间的随机整数。x 变成了 match 操作符的左操作数,右边是一个带有四个样例的表达式。最后一个例子是“捕获所有”任何大于2的数字的情况。样例(case)也被称为替代选择(alternatives)。

匹配表达式有一个值。

def matchTest(x: Int): String = x match {
  case 1 => "one"
  case 2 => "two"
  case _ => "many"
}
matchTest(3)  // many
matchTest(1)  // one

这个匹配表达式有一个字符串类型,因为所有的样例都返回字符串。因此,函数 matchTest 返回一个字符串。

匹配样例类

样例类对于模式匹配特别有用。

abstract class Notification

case class Email(sender: String, title: String, body: String) extends Notification
case class SMS(caller: String, message: String) extends Notification
case class VoiceRecording(contactName: String, link: String) extends Notification

Notification 是一个抽象的超类,它有三个具体的 Notification 类型,用样例类 EmailSMSVoiceRecording 实现。现在,我们可以对这些样例类进行模式匹配:

def showNotification(notification: Notification): String = {
  notification match {
    case Email(email, title, _) =>
      s"You got an email from $email with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
    case VoiceRecording(name, link) =>
      s"you received a Voice Recording from $name! Click the link to hear it: $link"
  }
}
val someSms = SMS("12345", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")

println(showNotification(someSms))  // prints You got an SMS from 12345! Message: Are you there?

println(showNotification(someVoiceRecording))  // you received a Voice Recording from Tom! Click the link to hear it: voicerecording.org/id/123

函数 showNotification 将抽象类型 Notification 作为一个参数,并匹配 Notification 的类型(例如,它可以判断它是 EmailSMS 还是 VoiceRecording)。在 Email(email, title, _) 中,字段 Emailtitle 在返回值中使用,但字段 body 使用 _ 而被忽略。

模式守卫

模式守卫只是简单的布尔表达式,用于使情况更具体。只要在模式之后添加if <boolean expression>

def showImportantNotification(notification: Notification, importantPeopleInfo: Seq[String]): String = {
  notification match {
    case Email(email, _, _) if importantPeopleInfo.contains(email) =>
      "You got an email from special someone!"
    case SMS(number, _) if importantPeopleInfo.contains(number) =>
      "You got an SMS from special someone!"
    case other =>
      showNotification(other) // nothing special, delegate to our original showNotification function
  }
}

val importantPeopleInfo = Seq("867-5309", "[email protected]")

val someSms = SMS("867-5309", "Are you there?")
val someVoiceRecording = VoiceRecording("Tom", "voicerecording.org/id/123")
val importantEmail = Email("[email protected]", "Drinks tonight?", "I'm free after 5!")
val importantSms = SMS("867-5309", "I'm here! Where are you?")

println(showImportantNotification(someSms, importantPeopleInfo))
println(showImportantNotification(someVoiceRecording, importantPeopleInfo))
println(showImportantNotification(importantEmail, importantPeopleInfo))
println(showImportantNotification(importantSms, importantPeopleInfo))

case Email(email, _, _) if importantPeopleInfo.contains(email) 中,只有当 Email 在重要人物的列表中才会匹配。

仅匹配类型

你可以像下面一样只匹配类型:

abstract class Device
case class Phone(model: String) extends Device{
  def screenOff = "Turning screen off"
}
case class Computer(model: String) extends Device {
  def screenSaverOn = "Turning screen saver on..."
}

def goIdle(device: Device) = device match {
  case p: Phone => p.screenOff
  case c: Computer => c.screenSaverOn
}

根据 Device 的类型,def goIdle 有不同的行为。当需要调用模式上的方法时,这是很有用的。将类型的第一个字母作为 case 标识符(在本例中是 pc)是一种约定。

密封类

特征和类可以被标记为 sealed,这意味着所有子类型必须在同一个文件中声明。这确保了所有子类型都是已知的。

sealed abstract class Furniture
case class Couch() extends Furniture
case class Chair() extends Furniture

def findPlaceToSit(piece: Furniture): String = piece match {
  case a: Couch => "Lie on the couch"
  case b: Chair => "Sit on the chair"
}

这对于模式匹配非常有用,因为我们不需要“捕获所有”的 case。

注意事项

Scala的模式匹配语句对于通过case类表示的代数类型的匹配非常有用。Scala还允许独立于case类的模式定义,在提取器对象中使用 unapply 的方法。

相关推荐