現在,我們想要寫一個對於數學計算表達式的解釋器。為了保持內容簡單,我們規定這里只有數字和“+”運算。這樣的表達式可以表示成一個類繼承關系。有一個抽象基類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是一個不被表達式其他地方使用的新變量。
