现在,我们想要写一个对于数学计算表达式的解释器。为了保持内容简单,我们规定这里只有数字和“+”运算。这样的表达式可以表示成一个类继承关系。有一个抽象基类Expr作为根类,和两个子类:Number和Sum。则表达式 1 + (3 + 7) 可以表示成:
new Sum(new Number(1) , new Sum(new Number(3) , new Number(7)))
现在,一个像这样的表达式要知道传入的是什么类型(Sum或者Number)。下面的实现提供了所有必要的方法。
类:Expr
abstract class Expr { def isNumber : Boolean def isSum : Boolean def numValue : Int def leftOp : Expr def rightOp : Expr }
类Number:
class Number(n : Int) extends Expr { def isNumber(): Boolean = true def isSum(): Boolean = false def numValue(): Int = n def leftOp(): Expr = error("Number.leftOp") def rightOp(): Expr = error("Number.rightOp") }
类Sum
class Sum(e1 : Expr , e2 : Expr) extends Expr { def isNumber(): Boolean = false def isSum(): Boolean = true def numValue(): Int = error("Sum.numValue") def leftOp(): Expr = e1 def rightOp(): Expr = e2 }
有了这些类,可以写出如下的计算函数
def eval( e : Expr) : Int = { if ( e.isNumber) e.numValue else if ( e.isSum) eval(e.leftOp) + eval(e.rightOp) else error("unrecognized expression kind") }
但是这样的定义很枯燥。更甚者,如果我们想增加一种新的表达式类型就会发生一些问题。例如,如果想增加一个类Prod用来表示products,那么不仅要实现这个新类,而且还要在Expr中加入对这种类型的判断。不得不改变已存在的代码对于一个增长的系统而言,经常会引入问题,包括版本和维护问题等。
对面向对象语言而言,这样的修改应该是非必须的。
考虑一下,如果用上面的方式,增加一个类型或者增加一个方法,会改动到很多类文件。我们要考虑的是,首先一个抽象基类的哪个子类要被用到,其次,构造参数是什么。因为这个问题很普通,Scala提供了case class来自动解决这个问题。
7.1 case class 和 case objects
定义是和普通的Class,object一样的,只是多了一个修饰符case 。如以下定义:
abstract class Expr case class Number( n :Int) extends Expr case class Sum(e1 : Expr , e2 : Expr) extends Expr
这样就引入了两个case class 。 定义为case,有以下的特性:
- Case Class 隐式地就有了一个构造方法,与类名一致。可以直接使用Sum(Sum(Number(1) , Number(2)) , Number(3))
- 隐式地有了toString , equals 和hashCode的实现。在重写的时候把这个类里所有的成员都用上。(书上说的是把类参数当成val去维护)
- 可以直接访问构造参数。否则在其他的类中,应该再提供类似于def n : Int 这样的方法来让外界访问这个变量。
- 样本类允许模式构造指向样本类的构造器(?翻译的可能有问题。回头应该再看看《Scala编程》中的说法,尼玛,没有!)
7.2 模式匹配
模式匹配是一个从C或Java的switch表达式泛化到类层级。Scala里用match 代替了switch表达式。match表达式里带有很多的case 。 如下是一个用模式匹配来实现的eval方法:
def eval(e : Expr) : Int = e match{ case Number(n) => n case Sum(l , r) => eval(l ) + eval(r) }
例子中有两个case。每个模式都与一个表达式相关联。通常,“模式”由以下几个部分构成:
- 样本类构造器,如Number,Sum,参数可以也是模式;
- 模式参数,如e1 , e2
- 通配符:_
- 字面量,如 1, true , "abc"
- 常量,如MAXINT , EmptySet
模式参数通常以小写字母开头,以便与常量标识符区分开。常量要以大写开头。每个变量名可能只在模式里出现一次。
模式匹配意义:模式匹配表达式:
e match {case p1 => e1 ... case pn => en}
根据顺序来进行匹配。如果没有匹配的,会抛出MatchError异常
模式匹配和方法: 在之前的例子中,我们把模式匹配定义在类层级外。当然也可以把它定义在类层级内。例如,可以把eval定义在基类Expr中,同样也用到模式匹配:
abstract class Expr{ def eval : Int = this match { case Number(n) => n case Sum(e1 , e2) => e1.eval + e2.eval } }
模式匹配匿名函数:到现在为止,case表达式总是跟match操作一起出现。但也可单独使用case表达式。代码块如下:
{case P1 => E1 ... case Pn => En}
只能被自己所见,并进行匹配。换言之,可以被看成为以下匿名函数的缩写:
(x => x match{case P1 => E1 ... case Pn => En})
x是一个不被表达式其他地方使用的新变量。