【Scala之旅】样例类与模式匹配
本节翻译自
综述:模式匹配是一个十分强大的机制,可以应用在很多场合: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
尽管 message2
和 message3
引用不同的对象,但每个对象的值是相等的。
复制
你可以通过使用 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
,但是 message4
的 body
被直接复制。
模式匹配
模式匹配是一种根据模式检查值的机制。一个成功的匹配也可以将一个值分解为它的组成部分。它是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
类型,用样例类 Email
、SMS
和 VoiceRecording
实现。现在,我们可以对这些样例类进行模式匹配:
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
的类型(例如,它可以判断它是 Email
、SMS
还是 VoiceRecording
)。在 Email(email, title, _)
中,字段 Email
和 title
在返回值中使用,但字段 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 标识符(在本例中是 p
和 c
)是一种约定。
密封类
特征和类可以被标记为 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
的方法。