Case Classes and Pattern Matching(分支类和模式匹配)

Case Classes and Pattern Matching(分支类和模式匹配)
     本章介绍分支类和模式匹配,这两个孪生兄弟可以帮你处理那些常规的,没有封装的数据结构。这两种构造在处理树形递归数据的时候非常有用。 如果你早先做过函数语言编程, 那模式匹配你可能比较熟悉,但分支类你可能就比较陌生了。分支类是scala在允许模式匹配支持对象的一种方法,这种方法可以节省很多废话。通常情况 下,你只需要增加一个case的关键字在每个分支类上就可以让这些分支类支持模式匹配了。
     本章从一个简单的分支类和模式匹配的例子讲起,然后就贯穿各种各样的模式,讲解封装类的角色,讨论选择类型,并且对模式识别在scala中一些很不明显的地方的应用做了演示。最后,演示了一个大一点的,比较实际的例子。
15.1 一个简单的例子 在深入专研模式匹配各种规则和细微差别之前,先通过看一个简单的例子得到大概的思想是比较值得的。比方说,你现在要做一个操作数学表达式的类库,它可能是你正在设计的领域语言的一部分。 处理这个问题的第一步是定义输入数据。为简化起见,我们先集中处理数学表达式中的变量,数字,一元操作和二元操作。这些罗列如下(列表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

上面的列表中有一个抽象的Expr类和四个子类。这四个子类就对应上面四个不同的表达式类型。上面5个类的内容都是空的,我们前面提到过,Scala允许你定义类时类的内容为空,只要你愿意,所以,class C和class C{}是一样的。
分支类
另外一个值得说明的事是列表15.1中每个子类都有一个case 修饰符,带有case 修饰符的类就叫分支类。用了这个修饰符,Scala编译器就可以在你的类上加上一些语法上的方便之处。首先,scala 编译器会在类上添加一个工厂方法,这意味着你可以用Var(“x”)而不用相对长一点的new Var(“x”)去构造一个Var对象。
scala> val v = Var("x")
v: Var = Var(x)
这个工厂方法在你嵌套使用的时候非常好用,因为没了那些讨厌的new关键字在代码中这一堆那一坨的出现,你可以对表达式的结构一目了然。
scala> val op = BinOp("+", Number(1), v)
op: BinOp = BinOp(+,Number(1.0),Var(x))
第二个语法上的方便之处是分支类的所有参数都隐式的有val前缀,所以这些参数都可以当成属性:
scala> v.name
res0: String = x
scala> op.left
res1: Expr = Number(1.0)
第三,编译器会帮这些分支类自动实现toString, hashCode,和equals方法。他们会自动打印,hash和比较分支类和那些参数的树形结构。由于==在scala中和equals是一样的,所以分支类总是结构比较:
scala> println(op)
BinOp(+,Number(1.0),Var(x))
scala> op.right == Var("x")
res3: Boolean = true
上面的三个方便之处增加了很多好处,然而代价却很小,你只需要用case修饰符,你的类和对象会变得稍微大点。之所以变大是因为生成了那些额外的方法。另外每个构造方法都增加了一个隐式的属性。但是,分支类最大的好处就是他们支持模式匹配。
模式匹配
比方说,你想简化刚才上面演示的那些代数表达式,那存在很多简化规则,我们先用下面三个简化规则做个演示。::
UnOp("-",UnOp("-",e)) => e // Double negation
BinOp("+", e, Number(0)) => e // Adding zero
BinOp("*", e, Number(1)) => e // Multiplying by one
用模式匹配,这些规则可以在列表15.2中的简化方法中起到核心作用,这个简化方法simplifyTop可以这么用:
scala> simplifyTop(UnOp("-",UnOp("-",Var("x"))))
res4: Expr = Var(x)
def simplifyTop(expr: Expr): Expr = expr match {
case UnOp("-",UnOp("-",e)) => e // Double negation
case BinOp("+", e, Number(0)) => e // Adding zero
case BinOp("*", e, Number(1)) => e // Multiplying by one
case _ => expr
}
simplifyTop方法的右边构成了一个match表达式,match可以对应于java中的switch,但match是在selector后面的。这就是说,selector match { alternatives }而不是switch (selector) { alternatives }。一个模式匹配包括一系列的匹配选择,每一个匹配选择都以case关键字开头。每一个匹配选择都包括一种模式以及一个或者多个表达式,如果匹配上了,后面的表达式就会运算求值。一个箭头符号分割匹配选择和表达式。一个match表达式会按每个匹配的顺序一个个的尝试匹配,第一个匹配的模式选中厚,箭头符号后的表示就会选中并执行。
一些常量模式如”+”,1等,通过==匹配。变量模式如e匹配任何值。在上面的例子中,注意前面三个例子最后的值都是e,这个e是相应模式中的一个变量。通配符(_)也匹配任何值,但是它并不引入任何变量去引用每个值。在列表15.2,注意match以一个默认的case结束,这个默认的case对表达式不做任何处理,只是简单返回表达式。
一个构造函数模式,如UnOp("-",e),匹配所有第一个参数是“-”第二个参数是e的的UnOp,注意,构造函数的参数本身也是模式,这意味着你可以用更准确的定义写深层嵌套的模式,如UnOp("-",UnOp("-",e))。想想看,同样的事情如果用visitor模式实现,那一定很难看,应该会是很冗长的句子,if语句,类型判断,类型转换等诸如此类的东西一大堆。
    match与switch的比较
match可以看做更通用的java风格的switch。Java的switch语句可以很自然的转化成match语句,这里,match语句中每一个模式都是常量,最后一个模式一定得是通配的(default)。但match和switch有三点不同,需要牢记在心。第一,match是一个scala表达式,它总会返回一个值;第二,scala的可选表达式从不会顺延到下一个分支;第三,如果没有任何模式匹配上,会有一个MatchError异常抛出。第三点意味着必须所有分支都要得到处理,就算什么都不做也得加一个默认的分支。列表15.3 给出了一个例子:
expr match {
case BinOp(op, left, right) =>
println(expr +" is a binary operation")
case _ =>
}
列表 15.3 ?一个带着空的默认分支的模式匹配

第二个空的默认分支是必须的。一旦expr参数不是BinOp,就会抛一个MatchError异常。在这个例子中,第二个分支没有任何代码,所以,如果这个分支运行的话,将什么也不做。两个分支的结果都是Unit value‘()’,当然,整个match表达式的结果也是‘()’。
15.2 各种各样的模式
前面我们演示了几种速成的模式,现在我们花点功夫逐个看看。模式的语法很简单,所以不用太担心。比如,前面列表15.1提到的表达式层级,模式Var(x)匹配任何变量的表达式,这个变量的名字就用x绑定。Var(x)在这里是一个表达式,同样的语法,其实也是一个对象,假如x已经绑定成一个变量的名字。由于模式的语法是透明的,所以主要的事情在于搞清楚究竟有多少不同种类的模式。
    通配模式
通配模式(_)匹配任何对象。你已经看到它用作默认的匹配一切的分支,比如下面这个模式:
expr match {
case BinOp(op, left, right) =>
println(expr +"is a binary operation")
case _ =>
}
通配模式也可以用于忽略一个对象你不太关心那部分的处理。比方说,前面那个例子实际上并不关注二元操作有哪些元素,它仅仅只是检查是不是一个二元操作。因此,上面的代码可以用通配模式匹配二元操作的元素。如列表15.4所示:
expr match {
case BinOp(_, _, _) => println(expr +"is a binary operation")
case _ => println("It's something else")
}
常量模式
一个常量模式仅匹配它自身。任何不变量都可以表示成常量模式,如5,true,”hello”,都是常量模式。同样,任何val变量和单根对象都可以用作常量。.比如Nil,一个单根对象,就是一个模式,它匹配空的List。列表15.5 演示了一些常量模式:
def describe(x: Any) = x match {
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _ => "something else"
}
列表15.5 ? 一个诸多常量模式的模式匹配
列表15.5的运行结果如下:
scala> describe(5)
res5: java.lang.String = five
scala> describe(true)
res6: java.lang.String = truth
scala> describe("hello")
res7: java.lang.String = hi!
scala> describe(Nil)
res8: java.lang.String = the empty list
scala> describe(List(1,2,3))
res9: java.lang.String = something else
变量模式
一个变量模式匹配任何对象,就像通配模式一样。与通配模式不同的是,scala会绑定变量到匹配的对象上。你可以在分支语句后面用绑定的变量去代表哪个匹配的对象。例子15.6就演示了一个带变量模式的match语句。后面那个默认的分支使用了变量模式,不管匹配的对象是什么,你都可以用somethingElse这个变量名去指代。
expr match {
case 0 => "zero"
case somethingElse => "not zero: "+ somethingElse
}
列表15.6一个带变量模式的模式匹配语句
变量模式或者常量模式?
    常量模式也有符号名,前面我们有一个用Nil表示一个常量模式的例子。这也有一个相关的例子,涉及到数学常量E (2.71828. . . )和Pi (3.14159. . . ):
scala> import Math.{E, Pi}
import Math.{E, Pi}
scala> E match {
case Pi => "strange math? Pi = "+ Pi
case _ => "OK"
}
res10: java.lang.String = OK
正如我们所预期的,E不匹配Pi,所以“strange math” 分支没有使用。
问题来了,Scala编译器是怎么知道Pi是一个常量而不是一个变量呢?Scala用一种简单的语法规则来区分,如果变量名是以小写字母开头,那就是一个变量模式,其他情况,那就是常量模式。创建一个小写的别名pi就可以看到不同了:
scala> val pi = Math.Pi
pi: Double = 3.141592653589793
scala> E match {
case pi => "strange math? Pi = "+ pi
}
res11: java.lang.String = strange math? Pi = 2.7182818...
这里编译器甚至不允许你增加一个默认分支,因为pi是一个变量模式,它匹配任何输入,所以它后面不会有别的分支了。
scala> E match {
case pi => "strange math? Pi = "+ pi
case _ => "OK"
}
<console>:9: error: unreachable code
case _ => "OK"
?
如果你非得用小写字母为一个常量模式命名,你可以用下面两个小技巧。第一,如果这个常量是某个对象的属性,你可以在前面带上修饰符。比如,pi是一个变量模型,但是obj.pi和this.pi是常量模型,尽管他们以小写字母开头。如果上面这招不行,你可以把这个变量用单引号括起来,’pi’也会被解析成一个常量模型:
scala> E match {
case `pi` => "strange math? Pi = "+ pi
case _ => "OK"
}
res13: java.lang.String = OK
如你所见,’’语法在scala中可以用于两个不同的场合,都是那些不寻常的环境下采用的。这里‘’可以把一个小写标示当做一个常量模式,前面6.10章节的时候,’’用于把一个关键字变成一个普通的标示符。如Thread.’yeild’()把yeild当成一个标示符而不是一个关键字。
    构造函数模式
构造函数模式是模式匹配的厉害所在。一个构造函数模式,比如 “BinOp("+", e, Number(0))”. 它由一个名字(BinOp)和几个参数("+", e,和 Number(0))构成。如果名字标示着一个分支类,这样的模式会先去检查这个对象是不是分支类的一个成员,然后再去检查构造函数的参数中的对象是否匹配给定的额外的模式。这些额外的模式意味着scala的模式支持嵌套匹配。这样的模式不仅检查顶层对象是否匹配,也会检查这个对象的内容是否进一步匹配更深的模式。既然额外的模式本身也可以是构造函数模式,所以你可以让他们检查任意深度嵌套的对象。如列表 15.7 中的模式会检查顶层的对象是一个BinOp,BinOp的第三个构造参数是一个number,而且number的值是0.这个模式只有一行,但是检查了三层的匹配。
expr match {
case BinOp("+", e, Number(0)) => println("a deep match")
case _ =>
}
列表15.7 ? 一个带构造函数模式的模式匹配语句

序列模式
你可以匹配序列类型,比如List,Array,就像你匹配分支类一样。用同样的语法,但现在你可以特指模式内任何数目的元素,如列表15.8所示,一个检查以0开头的3元素List 的序列模式:
expr match {
case List(0, _, _) => println("found it")
case _ =>
}
如果你不想匹配一个固定长度的序列,你可以用_*来表示这个模式。这个看起来比较搞笑的模式匹配序列里任意多的元素,包括0个元素的空序列。列表 15.9 演示了一个匹配任何一个以0开头的列表,不管它多长。
expr match {
case List(0, _*) => println("found it")
case _ =>
}
Tuple patterns
    你也可以匹配tuples。(a,b,c)匹配任意3-tuple。
def tupleDemo(expr: Any) =
expr match {
case (a, b, c) => println("matched "+ a + b + c)
case _ =>
}
传一个tuple进去让你看看结果:
scala> tupleDemo(("a ", 3, "tuple"))
matched a 3tuple
类型模式
你可以用类型模式方便的替代类型检测和类型转换语句。如列表15.11 所示:
def generalSize(x: Any) = x match {
case s: String => s.length
case m: Map[_, _] => m.size
case _ => 1
}
试试看:
scala> generalSize("abc")
res14: Int = 3
scala> generalSize(Map(1 >
'a', 2 >
'b'))
res15: Int = 2
scala> generalSize(Math.Pi)
res16: Int = 1
方法generalSize返回不同类型对象的size或者length,他的参数的类型是any,模式’s:String’是一个类型模式,它匹配每一个非空的String实例。模式的变量s指向那个String实例。注意,尽管s和x指向同一个值,但x的类型是any,但s的类型是String。所以你可以用s.length,但你不能用x.length,因为any类型没有length这个成员属性。 同样,你也可以用另外一种冗长绕弯的办法,通过类型检测和类型转换达到目的。Scala的语法和java在类型检测和类型转换上略有区别。String类型检测:expr.isInstanceOf[String]
String类型转换用expr.asInstanceOf[String]。聪明的你或许注意到scala中写类型检测和类型转换非常麻烦,这是故意的,因为scala不鼓励这样的做法,大多数时候你用类型模式的模式匹配能更好的解决问题。
类型擦除
你是否能检测一个元素是某种特定类型的map?看起来应当是手到擒来,比方说我们要检查给定的值是不是一个int To int的map,试试看先:
scala> def isIntIntMap(x: Any) = x match {
case m: Map[Int, Int] => true
case _ => false
}
warning: there were unchecked warnings; rerun
with
unchecked
for details
isIntIntMap: (Any)Boolean
解释器居然抛出一个‘未检查警告’,要想看到更详细的信息,得重新用另一个命令行启动scala解释器:scala unchecked,再来一遍:
scala> def isIntIntMap(x: Any) = x match {
case m: Map[Int, Int] => true
case _ => false
}
<console>:5: warning: non variable typeargument
Int in
type pattern is unchecked since it is eliminated by erasure
case m: Map[Int, Int] => true
Scala和java一样,用了泛型的擦除模型。这就是说,在运行时,我们不知道类型的信息。因此,没有办法知道一个给定的Map对象是通过两个int参数创建的。更别说是其他不同参数了。系统只知道map是某种任意类型的参数。我们可以通过执行isIntIntMap看看,scala> isIntIntMap(Map(1 >
1))
res17: Boolean = true
scala> isIntIntMap(Map("abc" >
"abc"))
res18: Boolean = true
第一个返回true,看起来正确,但第二个也返回true,那就让人惊讶了。为了警告你这个非直觉的运行时行为,编译器抛出了未检查警告,就像前面你看到的那样。
    类型擦除规则的唯一例外就是数组。因为他们和java中是一样处理的。数组的元素类型已经存在数组中,所以可以用于模式匹配。
scala> def isStringArray(x: Any) = x match {
case a: Array[String] => "yes"
case _ => "no"
}
isStringArray: (Any)java.lang.String
scala> val as = Array("abc")
as: Array[java.lang.String] = Array(abc)
scala> isStringArray(as)
res19: java.lang.String = yes
scala> val ai = Array(1, 2, 3)
ai: Array[Int] = Array(1, 2, 3)
scala> isStringArray(ai)
res20: java.lang.String = no

变量绑定模式
除了在一个独立的变量模式中绑定变量外,你还可以在任何其他模式中增加变量。你只需要简单的写下变量名,然后再写@符号,跟着是模式。这样就会创建一个变量绑定的模式。这样一种模式的意思是,如果模式匹配正常,而且成功命中,就设置这个变量到命中的 上,和一个简单的变量模式一样。
如列表15.13 所示,一个明显重复两次求绝对值的表达式,这样的表达式可以简化为只求一次绝对值。
expr match {
case UnOp("abs", e @ UnOp("abs", _)) => e
case _ =>
}
15.3 模式保护
有时候句法中的模式匹配不是足够精确,比方说,一个公式简化任务是把两个相同运算体的加法运算替换成运算题*2。比如e+e=e*2。在Expr语法树中,一个表达式可能是:BinOp("+", Var("x"), Var("x")) 将会被转化为:BinOp("*", Var("x"), Number(2))
那你可能这样定义规则:
scala> def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, x) => BinOp("*", x, Number(2))
case _ => e
}
<console>:10: error: x is already defined as value x
case BinOp("+", x, x) => BinOp("*", x, Number(2))
这个失败了,因为scala限制模式必须是线性的:一个模式变量只能在模式中出现一次。然而,你可以通过模式保护来解决这个问题。如列表15.14所示:
scala> def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, y) if x == y =>
BinOp("*", x, Number(2))
case _ => e
}
simplifyAdd: (Expr)Expr
一个模式保护跟在模式身后,以if开头。模式保护可以是任意的布尔表达式。只有在模式保护为真的时候才可能匹配。因此,第一个分支只会匹配运算符相等的情况。我们再多看几个其他的模式保护示例:
// match only positive integers
case n: Int if 0 < n => ...
// match only strings starting with the letter ‘a’
case s: String if s(0) == 'a' => ...

15.4 模式叠加
match语句中的模式是按照写下的顺序一个个尝试的。列表15.5演示了分支的匹配顺序:
def simplifyAll(expr: Expr): Expr = expr match {
case UnOp("-",UnOp("-",e)) =>simplifyAll(e) // ‘’
is its own inverse
case BinOp("+", e, Number(0)) =>simplifyAll(e) // ‘0’ is a neutral element for ‘+’
case BinOp("*", e, Number(1)) =>simplifyAll(e) // ‘1’ is a neutral element for ‘*’
case UnOp(op, e) =>UnOp(op, simplifyAll(e))
case BinOp(op, l, r) =>BinOp(op, simplifyAll(l), simplifyAll(r))
case _ => expr
}
上面的simplifyAll 将会简化表达式的每一层,不仅仅是简化顶层。从simplifyTop衍生出simplifyAll,主要增加了分支4和分支5,对任意的unary和binary表达式都提供了支持。第4个分支,UnOp(op,e),匹配任意的一元操作。一元操作符和运算对象可以是任意的,分别绑定到变量op和e上。通过对e循环调用simplifyAll再次简化这个一元操作。第5个分支也类似,匹配所有二元操作表达式。
在这个例子中,值得注意的是,匹配所有的分支一定要在匹配特定表达式的分支之后。如果不按照这个顺序,匹配所有的分支会在匹配特定表达式分支之前执行,很多时候,你做这样的尝试的时候编译器会发出抱怨。
比如说,下面这个match表达式将不会通过编译,因为第二个分支能匹配的,肯定第一个分支也能匹配:
scala> def simplifyBad(expr: Expr): Expr = expr match {
case UnOp(op, e) => UnOp(op, simplifyBad(e))
case UnOp("-",UnOp("-",e)) => e
}
<console>:17: error: unreachable code
case UnOp("",
UnOp("",
e)) => e
15.5 密封类
当你写一个模式匹配表达式的时候,你需要确定你覆盖了所有可能的分支。有时候,你通过在最后增加一个默认分支达到目的,但这仅当默认分支的行为有意义的时候才比较合适。如果默认分支根本没有什么有意义的处理,你会怎么做呢?怎样才能让你确信所有分支都覆盖了呢?
    事实上,你可以让scala编译器帮你探测模式组合中缺失的部分。为了做到这一点,编译器需要被告知哪些是可能的分支。但通常来说,新的分支类可以在任何时间和任何编译单元中定义,scala无法做到这一点。举个例子,没有什么能够阻止你在一个新的编译单元给Expr增加第五个分支类。一种替代的选择就是让分支类的父类封口,变成密封类。密封类出了同文件下的子类,不会再有新的子类。这对模式匹配非常有用,这意味着你只需要看好已知的子类就ok了。此外,编译器对你的支持也更到位了。如果你匹配的分支类继承自一个密封类,编译器如果发现有缺失的模式组合,将会给出警告信息。
    因此,如果你写一个分层的类结构用于模式匹配,你应该考虑密封,简单的在顶层类的定义前加一个sealed关键字就行了。其他程序员在模式匹配中用你的分层类结构就会放心多了。从这看来,sealed这个关键字,通常也是模式匹配的一个执照。列表15.16展示了一个密封的Expr类。
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
现在定义一个match表达式,让一些可能的分支漏掉先:
def describe(e: Expr): String = e match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
回车后,你会看到一个编译警告,如下:
warning: match is not exhaustive!
missing combination UnOp
missing combination BinOp
这样的警告会告诉你,你的代码会抛出一个MatchError异常,因为一些可能的模式没有处理。警告指向潜在的运行时错误,所以,这通常是一个很有用的帮助信息。但是,有时候你的上下文环境告诉你,表达式不是Number就是Var,所以你知道根本不会有MatchError异常抛出,为了让这个异常消失,你需要增加一个匹配所有的分支:
def describe(e: Expr): String = e match {
case Number(_) => "a number"
case Var(_) => "a variable"
case _ => throw new RuntimeException // Should not happen
}
这样做是可以的,但是解决办法不够理想。为了让编译器闭嘴,加上这么一行永远不会执行的代码,你可能很不乐意这么做。还有一个更加轻量级的选择,加一个 @unchecked 的注解到selector上,就像下面这么做
def describe(e: Expr): String = (e: @unchecked) match {
case Number(_) => "a number"
case Var(_) => "a variable"
}
注解将在第25章详细讲述。通常,你加一个注解的方式和你加一个类型的方式是一样的,在表达式后加一个:,然后加上注解(当然,注解前有@)。举个例子,你要在变量e上加@unchecked 注解,格式就是“e: @unchecked”。@unchecked注解对模式匹配有特定的意思,如果模式匹配的selector里有这个注解,编译器就不会对这个模式匹配进行穷举检查。
15.6 Option 类型
Scala对于可选值有一个标准的类型,可选值有两种形式,Some(x),当x确实有值的时候;或者是None,当x是得不到的时候。可选值什么时候产生呢?scala的集合做一些标准操作时会产生可选值。举个例子,Scala’s Map类的get方法返回Some(value),如果给定key的value得到的话; None,如果给定的key在Map中就没有定义。看看下面的例子:
scala> val capitals =Map("France" >"Paris", "Japan" >"Tokyo")
capitals:
scala.collection.immutable.Map[java.lang.String,java.lang.String] = Map(France >Paris, Japan >Tokyo)
scala> capitals get "France"
res21: Option[java.lang.String] = Some(Paris)
scala> capitals get "North Pole"
res22: Option[java.lang.String] = None
拆卸可选值最常用的招数就是模式匹配:
scala> def show(x: Option[String]) = x match {
case Some(s) => s
case None => "?"
}
show: (Option[String])String
scala> show(capitals get "Japan")
res23: String = Tokyo
scala> show(capitals get "France")
res24: String = Paris
scala> show(capitals get "North Pole")
res25: String = ?
Option类型在scala程序中有着广泛的应用。比起java中常见的null表示空,比如java.util.HashMap返回value(如果有值的话)或者null(如果无值),java中的这种做法容易出错,因为在程序中很难跟踪一个变量是否允许为空。如果一个变量允许为空,你必须牢记,每次你用它的时候都需要检查它是否为空。如果你忘记了,你可能会在运行时抛出一个NullPointerException。因为这样的异常并不是经常发生的,所以在测试中很难发现。但在scala中,这个办法就行不通了。因为scala中的hashmap可以存储值的类型。但null不是一个值类型的有效元素,例如,一个HashMap[Int, Int]不可能返回null去标示“没有任何元素”。
通过约定,scala鼓励大家使用Option类型去标示可选值。这种做法比起java的null有一些优势,首先,Option[String]变量比起String变量更易读,String变量可能为空。更重要的是,前面提到的程序运行时错误在scala里面会是一个编译期的类型错误。如果一个变量时Option[String]类型,你把它用成String,scala程序根本就编译通不过。
15.7 模式的各种应用
    模式在scala很多地方都有应用,不仅仅在模式匹配表达式中,让我们看看模式在其他地方的应用。
模式用于变量定义
每次你定义一个val或则var时,你可以用模式取代一些简单的类型标示。比如,你可以将一个tuple的各个部分赋值到对应的变量中,如列表15.17所示:
scala> val myTuple = (123, "abc")
myTuple: (Int, java.lang.String) = (123,abc)
scala> val (number, string) = myTuple
number: Int = 123
string: java.lang.String = abc
这个结构在分支类中非常有用。如果你知道你用到的分支类的精确定义,你可以用模式来解构它:
scala> val exp = new BinOp("*", Number(5), Number(1))
exp: BinOp = BinOp(*,Number(5.0),Number(1.0))
scala> val BinOp(op, left, right) = exp
op: String = *
left: Expr = Number(5.0)
right: Expr = Number(1.0)
分支序列作为偏函数
一个分支的序列用大括号括起来之后,可以用于任何方法可以用的地方。本质上,一个分支序列就是一个方法定义,只是更广义的方法定义而已。不像方法,只有一个入口和一个参数列表,一个分支序列有多个入口,每个都有自己的参数列表。每个分支就是一个方法入口,参数是模式指定的。每个入口就是分支的右边。如下例:
val withDefault: Option[Int] => Int = {
case Some(x) => x
case None => 0
}
这个方法有两个分支。第一个分支匹配Some,然后返回Some里面的数值。第二个返回0.如何调用这个方法看下面:
scala> withDefault(Some(10))
res25: Int = 10
scala> withDefault(None)
res26: Int = 0
这个功能在actor库中非常有用,在第30章中有详细阐述。下面列一些典型的actors的代码,它传了一个模式匹配到react方法里面。:
react {
case (name: String, actor: Actor) => {
actor ! getip(name)
act()
}
case msg => {
println("Unhandled message: "+ msg)
act()
}
}
第二个扩展也值得一说,一个分支序列给你一个偏函数。如果你将这个方法作用到它不支持的值上,它会产生一个运行时异常。举个例子,这里有一个部分函数返回一个整数list的第二个元素。
val second: List[Int] => Int = {
case x :: y :: _ => y
}
当你编译的时候,编译器抱怨匹配没有穷尽。:
<console>:17: warning: match is not exhaustive!
missing combination Nil
这个方法当你传一个三元素的列表时,没什么问题,当你传一个空列表的时候就出问题了:
scala> second(List(5,6,7))
res24: Int = 6
scala> second(List())
scala.MatchError: List()
at $anonfun$1.apply(<console>:17)
at $anonfun$1.apply(<console>:17)
如果你想检查一个偏函数是否良好定义了,你必须先告诉编译器,你正在处理偏函数。类型List[int]包括了所有参数是List[int]的函数,不管是普通函数还是偏函数。只包括偏函数的方法是这么定义的PartialFunction[List[Int],Int]所以,这个方法的第二版改进如下:
val second: PartialFunction[List[Int],Int] = {
case x :: y :: _ => y
}
偏函数有一个方法名为isDefinedAt, 这个可以检查这个方法在某个特殊参数的时候是否良好定义了。在我们上面这个例子,这个方法对于最少有两个元素的List都是明确定义了的:
scala> second.isDefinedAt(List(5,6,7))
res27: Boolean = true
scala> second.isDefinedAt(List())
res28: Boolean = false
典型的偏函数就是模式匹配的偏函数定义。事实上,这样的表达式会被scala编译器解析两次,第一次是实现整个方法,第二次是看这个方法是否明确定义了。举个例子,上面方法的秒素{ case x :: y :: _ => y } 会解析成下面这样子:
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
}
}
第二次解析当一个函数体是偏函数的时候。如果定义的类型只是Function1,或者啥都不定义,这个函数体就会被完全解析,没有那个第二次解析。
通常,你应该尽可能的用完全函数,因为偏函数会产生运行时错误,这个运行时错误编译器也帮不了你。但有时候偏函数非常有用,当你确定不会发生没有处理的值的时候,或者你也可先调用isDefinedAt检查一下,然后调用偏函数作为一个替代选择。react例子就是后者的应用。参数是一个偏函数,精确的定义了哪些调用者想处理的消息。
模式在表达式中
你也可以在表达式中用模式。如列表15.18.所示。每对都符合模式 (country, city),
两个变量country和city在前面有定义。列表15.18的模式比较特殊,因为(country, city)不会失败。事实上,capitals产生了一个键值对序列。所以你可以肯定每个产生的键值对匹配(country, city)模式。
scala> for ((country, city) <capitals)
println("The capital of "+ country +" is "+ city)
The capital of France is Paris
The capital of Japan is Tokyo
但也可能一个模式并不匹配产生的值。列表15.19 就有一个这样的例子:
scala> val results = List(Some("apple"), None,
Some("orange"))
results: List[Option[java.lang.String]] = List(Some(apple),
None, Some(orange))
scala> for (Some(fruit) <results)
println(fruit)
apple
orange
从这个例子可以看到,没有匹配的值被抛弃了。比如,第二个元素None不符合模式Some(fruit),所以它没有在输出中显示出来。
15.8 一个大的例子
    学了不同形式的模式之后,你可能对再一个大一点的例子中应用它们很感兴趣。这个大点的例子就是写一个表达式格式化类,把一个代数表达式写成一个分数式的形式。比如除法“x / x + 1” 应该被垂直分层打印。分子在上,分母在下,像下面这样:
 
((a / (b * c) + 1 / n) / 3)应该表示成:
 
从这个例子来看,我们的主要类(ExprFormatter)看起来要做不少布局的杂事,所以用第十章的布局库是有意义的。我们当然也会用这章前面部分提到的分支类。所有代码在列表15.20 和 15.21中。第一步先关注水平布局。表达式的结构如:
BinOp("+",
BinOp("*",
BinOp("+", Var("x"), Var("y")),
Var("z")),
Number(1))
应该打印 (x + y) * z + 1. 注意,括号对(x+y)是必须的,但对于(x+y)*z是可选的。为了让布局清晰易读,目标应该是括号能省则省,不能省的一个都不省。
    为了知道哪里要放括号,代码需要知道操作符的相对优先级。所以,现在我们先处理优先级问题是个好的选择。我们可以把优先级表示成下面的map形式:
Map("|" >0, "||" >0,"&" >1, "&&" >1, ...)
但是,这将牵涉到一些预先计算操作符的优先级。一个更方便的办法是仅仅定义一组操作符,按优先级的顺序排列,然后按这个顺序处理表达式。列表15.20 展示了相关代码。
优先级就是操作符在map中的序列号,从0开始。通过一个for循环,首先找到操作符组。操作符组所在的序列号就是操作符组的优先级值,然后通过操作符组里面操作符的序列号找到操作符。这样可以产生一个(op, i)对。其中,op是操作符,i是优先级值。现在我们固定了二元操作符的优先级,除了”/”这个操作符外。当然,一元操作符的优先级值定义也是有意义的。一元操作符的优先级高于所有二元操作符,所以,可以设定一元操作符的优先级值unaryPrecedence是操作符组数组的长度,比*,%操作符的优先级大一。
分数线操作符的优先级值与其他操作符不太一样,因为它用于垂直布局。把这个值设为-1对我们来说比较方便。fractionPrecedence=-1
经过这些准备,现在我们可以着手主要的format方法了。这个方法带两个参数,一个表达式e,e的优先级值enclPrec(直接封闭这个表达式操作符的优先级值,没有操作符的话,优先级值就是0)。这个方法产生了一个布局元素代表一个二维字母数组。列表15.21 展现了ExprFormatter的其它部分,有三个方法,第一个方法,stripDot, 是一个帮助方法。第二个方法,私有的format方法,做了很多格式化表达式的工作,第三个方法,公有的format方法,将表达式转化为格式化形式。
    私有的format方法是用模式匹配干活的。模式匹配语句有5个分支,我们分别讨论每一个分支。第一个分支:
case Var(name) =>
elem(name)
如果表达式是一个变量,结果是变量名格式化后的元素。
第二个分支:
case Number(num) =>
def stripDot(s: String) =
if (s endsWith ".0") s.substring(0, s.length 2)
else s
elem(stripDot(num.toString))
如果表达式是一个数字,结果是数字格式化后的元素。stripDot函数处理了浮点数。
第三个分支: case UnOp(op, arg) => elem(op) beside format(arg, unaryPrecedence) 如果表达式是一个一元操作UnOp(op, arg), 结果总是可以看做这两部分,操作符op和格式化后的参数arg。这意味着如果arg是一个二元操作,这个二元操作(不是除操作的话)在显示的时候一定得带括号。 第四个分支: case BinOp("/", left, right) => val top = format(left, fractionPrecedence) val bot = format(right, fractionPrecedence) val line = elem('', top.width max bot.width, 1) val frac = top above line above bot if (enclPrec != fractionPrecedence) frac else elem(" ") beside frac beside elem(" ") 如果表达式是一个分式,那么应该会有一条横线在中间把操作符的左和右分开,横线的宽度是格式化后左右两个操作对象的最大宽度。如果这个分式中还有分数,那么分割线要左右多加一个空格的长度。"(a/b)/c"这个式子怎么表示想想就清楚。 第五个分支是: case BinOp(op, left, right) => val opPrec = precedence(op) val l = format(left, opPrec) val r = format(right, opPrec + 1) val oper = l beside elem(" "+ op +" ") beside r if (enclPrec =op的优先级,右边的条件则必须是>。这个机制确保了括号的正确性: BinOp("-",Var("a"), BinOp("-",Var("b"), Var("c")))可以由此正确的标为 “a-(b-c)”。这样的得到左右两个操作符的中间结果后,再插入操作符op。这个时候这个二元操作的中间结果就可以插入到整个表达式中了,如果整个表达式前面的操作符的优先级

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值