ch 15
样例类是Scala用来对对象进行模式匹配而并不需要大量的样板代码的方式。
本章内容:
- 样例类和模式匹配的例子
- Scala支持的各种模式
- 密封类(sealed class)
Option
类型
15.1 一个简单的例子
实现一个操作算术表达式的类库:
// 定义输入数据
abstract class Expr // 省去了空定义体的{}
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class Unop(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
样例类
带有case
修饰符的类。
- 添加一个跟类同名的工厂方法。可以用
Var("x")
构造对象。 - 参数列表中的参数都隐式获得了一个
val
前缀,它们会被当作字段。如v.name
、op.left
等。 - 编译器会帮助我们实现
toString
、hashCode
和equals
方法。 - 编译器还会添加一个
copy
方法用于制作修改过的,除了一两个属性不同之外其余完全相同的该类的新实例。例如:op.copy(operator = "-")
。用带名字的参数给出想要做的修改,没有给出的参数将使用老对象中的原值。
模式匹配
假定我们想简化前面的算术表达式。可用的简化规则如下:
UnOp("-", UnOp("-", e)) => e // -(-x) => +x
BinOp("+", e, Number(0)) => e // x+0 => x
BinOp("*", e, Number(1)) => e // x*1 => x
使用模式匹配实现simplifyTop
函数:
def simplifyTop(expr: Expr): Expr = expr match {
case UnOp("-", UnOp("-", e)) => e
case BinOp("+", e, Number(0)) => e
case BinOp("*", e, Number(1)) => e
case _ => expr
}
Scala的match
和Java的switch
的区别:
match
是一个表达式(也就是说它总是能得到一个值)- Scala的可选分支不会fall through到下一个
case
- 如果没有模式匹配上,会抛出一个
MatchError
异常
expr match {
case BinOp(op, left, right) =>
println(expr + " is a binary operation")
case _ => /* 什么都不会发生 结果是 unit 值 */
}
15.2 模式的种类
通配模式
1. 用于缺省、捕获所有的可选路径
case _ => // default case
2. 用于忽略某个对象中不关心的局部
expr match {
case BinOp(_, _, _) => println(expr + " is a binary operation")
case _ => println("It's something else")
}
常量模式
仅匹配自己。任何字面量、val
或单例对象都可以作为常量模式使用。例如5
是常量,Nil
是单例对象。
def describe(x: Any) = x match {
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _ => "something else"
}
describe(5)
describe(true)
describe("Hello!")
变量模式
匹配任何对象,Scala将对应的变量绑定成匹配上的对象。
expr match {
case 0 => "zero"
case somethingElse => "not zero: " + somethingElse
}
变量还是常量?
常量模式也可以有符号形式的名称。
import math.{E, Pi}
E match {
case Pi => "strange math? Pi = " + Pi
case _ => "OK"
}
E
并不匹配Pi
。Scala采用了一个简单的词法规则来区分:一个以小写字母打头的简单名称会被当作模式变量处理;所有其它引用都是常量。
val pi = math.Pi
E match {
case pi => "strange math? Pi = " + pi
}
这里编译器不允许添加一个默认的case
。由于pi
是变量模式,它将会匹配所有的输入,因此不可能走到后面的case
:
E match {
case pi => "strange math? Pi = " + pi
case _ => "OK"
}
如果需要,仍然可以用小写名称作为模式常量。
- 如果常量是某个对象的字段,则可以在字段名前面加上限定词。例如
this.pi
或obj.pi
。 - 可以用反引号将这个名称包起来。
E match {
case `pi` => "strange math? Pi = " + pi
case _ => "OK"
}
构造方法模式
例如BinOp("+", e, Number(0))
。
它由一个名称(BinOp
)和一组模式组成。假定这里的名称指定的是一个样例类,这样的一个模式将首先检查被匹配的对象是否是以这个名称命名的样例类的实例,然后再检查这个对象的构造方法参数是否匹配这些额外给出的模式。
expr match {
case BinOp("+", e, Number(0)) => println("a deep match")
case _ =>
}
序列模式
也可以和序列类型如List
或Array
匹配。
expr match {
// 以0开始的3元素列表
case List(0, _, _) => println("found it")
case _ =>
}
expr match {
// _*可以匹配序列中任意数量的元素
case List(0, _*) => println("found it")
case _ =>
}
元组模式
def tupleDemo(expr: Any) =
expr match {
case (a, b ,c) => println("matched " + a + b + c)
case _ =>
}
tupleDemo("a ", 3, "-tuple")
matched a 3-tuple
带类型的模式
替代类型测试和类型转换。
def generalSize(x: Any) = x match {
case s: String => s.length
case m: Map[_, _] => m.size
case _ => -1
}
generalSize("abc")
generalSize(Map(1 -> 'a', 2 -> 'b'))
generalSize(math.Pi)
Scala的类型测试和转换
- 类型测试:
expr.isInstanceOf[String]
- 类型转换:
expr.asInstanceOf[String]
// 重写case s: String => s.length这一表达式
if (x.isInstanceOf[String]) {
val s = x.asInstanceOf[String]
s.length
} else ...
类型擦除
测试某个值是否是Int
到Int
类型的映射。
def isIntIntMap(x: Any) = x match {
case m: Map[Int, Int] => true
case _ => false
} // wrong
Scala采用了擦除式的泛型,就跟Java一样。这意味着在运行时并不会保留类型参数的信息。这么一来,我们在运行时就无法判断某个给定的Map
对象是用两个Int
的类型参数创建的,还是其他什么类型参数创建的。系统能做的只是判断某个值是某种不确定类型参数的Map
。可以把isintlntMap
应用到不同的Map
类实例来验证这个行为:
isIntIntMap(Map(1 -> 1))
isIntIntMap(Map("abc" -> "abc"))
对于这个擦除规则唯一的例外是数组,因为Java和Scala都对它们做了特殊处理。数组的元素类型是跟数组一起保存的,因此我们可以对它进行模式匹配。例如:
def isStringArray(x: Any) = x match {
case a: Array[String] => "yes"
case _ => "no"
}
val as = Array("abc")
isStringArray(as)
val ai = Array(1, 2, 3)
isStringArray(ai)
变量绑定
对模式添加变量。
expr match {
// 若这个匹配成功了 则 e = UnOp("abs", _)
case UnOp("abs", e @ UnOp("abs", _)) => e
case _ =>
}
15.3 模式守卫
def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, y) if x == y => // 不能写 BinOp("+", x, x)
BinOp("*", x, Number(2))
case _ => e
}
// 匹配正整数
case n: Int if 0 < n => ...
// 匹配'a'开头的字符串
case s: String if s(0) == 'a' => ...
15.4 模式重叠
模式会按照代码中的顺序逐个被尝试。
def simplifyAll(expr: Expr): Expr = expr match {
case UnOp("-", UnOp("-", e)) =>
simplifyAll(e)
case BinOp("+", e, Number(0)) =>
simplifyAll(e)
case BinOp("*", e, Number(1)) =>
simplifyAll(e)
// 捕获所有的 case 出现在更具体的简化规则之后
case UnOp(op, e) =>
UnOp(op, simplifyAll(e))
case BinOp(op, l, r) =>
BinOp(op, simplifyAll(l), simplifyAll(r))
case _ => expr
}
15.5 密封类
密封类除了在同一个文件中定义的子类之外,不能添加新的子类。
sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class Unop(operator: String, arg: Expr) extends Expr
case class BinOp(operator: String, left: Expr, right: Expr) extends Expr
若定义了一个漏掉了某些可能的case
的模式匹配,则编译器会发出警告:
def describe(e: Expr): String = e match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
// use @unchecked to suppress warning
def describe(e: Expr): String = (e: @unchecked) match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
Option
类型
Option`可以有两种形式:`Some(x)`和`None
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals get "France"
capitals get "North Pole"
def show(x: Option[String]) = x match {
case Some(s) => s
case None => "?"
}
show(capitals get "France")
show(capitals get "Japan")
show(capitals get "North Pole")
15.7 到处都是模式
变量定义中的模式
例如,可以将一个元组中的每个元素赋值给不同变量。
val myTuple = (123, "abc")
val (number, string) = myTuple
处理样例类时非常有用。如果你知道要处理的样例类是什么,就可以用 一个模式来析构它。
val exp = new BinOp("*", Number(5), Number(1))
val BinOp(op, l, r) = exp
作为偏函数的case
序列
用{}
包起来的一系列case
语句可以用在任何允许出现函数字面量的地方。本质上讲,case
序列就是一个函数字面量。case
序列可以有多个入口,多个参数列表。
val withDefault: Option[Int] => Int = {
case Some(x) => x
case None => 0
}
withDefault(Some(10))
withDefault(None)
通过case
序列得到的是一个偏函数(partial function)。若将其应用到它不支持的值上,会产生一个运行时异常。
val second: List[Int] => Int = {
case x :: y :: _ => y
}
second(List(5, 6, 7))
second(List())
若想检查某个偏函数是否对某个入参有定义,必须首先告诉编译器你要处理的是偏函数。
val second: PartialFunction[List[Int], Int] = {
case x :: y :: _ => y
}
second.isDefinedAt(List(5, 6, 7))
second.isDefinedAt(List())
new PartialFunction[List[Int], Int] {
def apply(xs: List[Int]) = xs match {
case x :: y :: _ => y
}
def isDefinedAt(xs: List[Int]) = xs match {
case x :: y :: _ => true
case _ => false
}
}
for
表达式中的模式
for
表达式可以从映射中接收key-value pairs,使其与模式匹配。例如:
for ((country, city) <- capitals)
println("The capital of " + country + " is " + city)
The capital of France is Paris
The capital of Japan is Tokyo
可能存在某个模式不能匹配某个生成的值的情况。
val results = List(Some("apple"), None, Some("orange"))
for (Some(fruit) <- results) println(fruit)
apple
orange
可见生成的值当中那些不能匹配给定模式的值会被直接丢弃。