摘要
本文將詳細介紹 Scala 中兩個非常重要的概念 case class 和 pattern matching,并通過具體的案例來說明兩者的具體用途。
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,和四個子類:Var(變量)、Number(數字)、UnOp(一元運算)、BinOp(二元運算),且每個子類使用 case 關鍵字修飾
case class
簡單的說,使用 case 修飾的 class 就是 case class,使用 case 會給我們帶來很多方便的地方:
1、自動為該類添加了一個和類名一樣的工廠方法,所以在實例化的時候我們可以直接寫成 Var("x"),不再寫成 new Var("x"),這種寫法在嵌套的情況下非常簡潔,例如:
val op = BinOp("+", Number(1.0), Var("x"))
2、所有 case class 的參數被隱式的添加上了 val 前綴,也就是說我們可以直接像這樣訪問參數
op.operator
3、編譯器添加了 toString、hashCode、equals 方法的實現
println(op) //BinOp(+,Number(1.0),Var(x))
4、編譯器添加了一個 copy 方法,可以通過修改特定的參數生成一個新的 case class 實例
op.copy(operator = "-") //BinOp = BinOp(-,Number(1.0),Var(x))
pattern matching
接下來我們要實現三種運算律:負負得正、任何數加 0 還是它本身、任何數乘以 1 還是它本身,即如下形式:
UnOp("-", UnOp("-", null)) => null // Double negation
BinOp("+", null, Number(0)) => null // Adding zero
BinOp("*", null, Number(1)) => null // Multiplying by one
具體的實現如下:
object Expr {
def main(args: Array[String]): Unit = {
val result = simplifyTop(UnOp("-", UnOp("-", Number(2.0))))
println(result) //Number(2.0)
}
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
}
}
可以看出 Scala 中模式匹配(pattern matching)使用如下形式:
selector match { alternatives }
而 Java 中的 switch 形如:
switch (selector) { alternatives }
case 后面是匹配的某種情況,匹配的種類有很多,這里我們使用的是 Constructor patterns,=> 后面是匹配成功后執行的操作,但是不管執行什么操作返回的結果都必須是函數的返回值類型,這里是 Expr;最后一行 case _ 中的 _ 是通配符,表示匹配任何值,匹配成功后不執行任何操作,只是將 expr 返回
對比 Java 中的 switch,match 有三個不同點:第一、在 Scala 中 match 是一個表達式,這說明 match 是有返回值的;第二、只要匹配到一個,下面的語句就不會執行;第三、如果沒有匹配到任何項就會剖出一個名為 MatchError 的異常,所以要確保能夠匹配到值。
2、匹配的種類
Wildcard patterns
即通配符匹配 _ ,匹配所有的情況
Constant patterns
即字面量匹配,代碼說明一切:
scala> def describe(x: Any): Any = x match {
| case 5 => "five"
| case true => "truth!"
| case "spark" => "the future!"
| case Nil => "the empty list"
| case _ => "something else"
| }
describe: (x: Any)Any
測試結果:
scala> describe(5)
res10: Any = five
scala> describe(true)
res11: Any = truth!
scala> describe("spark")
res12: Any = the future!
scala> describe(Nil)
res13: Any = the empty list
scala> describe(Array(1,2,3,4,5,6))
res14: Any = something else
scala> describe(1)
res15: Any = something else
Variable patterns
variable patterns 跟通配符(_)類似可以匹配任何對象,跟通配符不同的是 Scala 會將需要匹配的對象傳遞給 case 后面的變量,測試如下:
scala> 1 match {
| case v => "the variable is " + v
| }
res19: String = the variable is 1
scala> "scala" match {
| case v => "the variable is " + v
| }
res20: String = the variable is scala
scala> case class Person(name: String)
defined class Person
scala> Person("Jack") match {
| case v => "the variable is " + v
| }
res21: String = the variable is Person(Jack)
另外需要注意的是,上例中的匹配不需要 case _,否則會報錯:
scala> 1 match {
| case v => "the variable is " + v
| case _ => "something else"
| }
<console>:14: warning: unreachable code
case _ => "something else"
因為永遠不可能匹配到 _
如果要匹配一個變量的值,需要使用“`”符號,例如:
scala> val pi = math.Pi
pi: Double = 3.141592653589793
scala> 1 match {
| case `pi` => "success"
| case _ => "fail"
| }
res24: String = fail
*如果不使用“”符號,進行的就是 variable pattern,使用的話就是 constant pattern** *“
”符號的另外一個作用就是將關鍵字變成標識符
Constructor patterns
也就是最開始的例子中使用的匹配模式:case BinOp("+", e, Number(0)) => e
,首先要檢查這個對象是否為指定的 case class 的成員,然后檢查構造方法的參數是否符合額外匹配(deep matches:意味著進行深層次的匹配),例如上例中的 "+" 是 constant pattern,e 是 variable 匹配,而 Number(0) 又是一個 constructor pattern...
Sequence patterns
和 constructor patterns 類似,但是可以指定任意數量的元素,例如匹配以 0 開頭的總共有 3 個元素的 List
scala> List(0, 1, 2) match {
| case List(0, _, _) => "success"
| }
res26: String = success
匹配以 0 開頭任意元素的數組:
scala> Array(0, 1, 2, 3, 4, 5, 6) match{
| case Array(0, _*) => "success"
| }
res27: String = success
Tuple patterns
匹配 Tuple,Tuple 中的元素類型可以不同
scala> (1, "scala", true) match {
| case (a, b, c) => "matched " + a + b + c
| }
res30: String = matched 1scalatrue
Typed patterns
即類型匹配,例如:
scala> def generalSize(x: Any) = x match{
| case s: String => s.length
| case m: Map[_, _] => m.size
| case _ => -1
| }
generalSize: (x: Any)Int
scala> generalSize("abc")
res31: Int = 3
scala> generalSize(Map(1 -> 'a', 2 -> 'b'))
res32: Int = 2
scala> generalSize(123)
res33: Int = -1
也可以寫成如下形式:
if (x.isInstanceOf[String]) {
val s = x.asInstanceOf[String]
s.length
} else ...
其中 isInstanceOf 判斷是否為某種類型,而 x.asInstanceOf[String] 將 x 轉換成 String 類型
下面來看一個非常重要的概念:
類型擦除(Type erasure)
先看測試:
scala> def isIntIntMap(x: Any) = x match {
| case m: Map[Int, Int] => true
| case _ => false
| }
<console>:12: warning: non-variable type argument Int in type pattern scala.collection.immutable.Map[Int,Int] (the underlying of Map[Int,Int]) is unchecked since it is eliminated by erasure
case m: Map[Int, Int] => true
^
isIntIntMap: (x: Any)Boolean
warning 說的很清楚,在運行時范型會被擦出,所以不能判斷 Map 的具體元素的類型是否匹配,但是 Array 除外,因為數組存儲的時候將值和類型一起進行存儲:
scala> def isStringArray(x: Any) = x match{
| case a: Array[String] => "success"
| case _ => "failed"
| }
isStringArray: (x: Any)String
scala> isStringArray(Array("scala", "spark"))
res36: String = success
scala> isStringArray(Array(1, 2))
res37: String = failed
Variable binding
直接看代碼:
scala> UnOp("abs", UnOp("abs", Number(1.0))) match {
| case UnOp("abs", e @ UnOp("abs", _)) => e
| case _ => "something else"
| }
res40: java.io.Serializable = UnOp(abs,Number(1.0))
注意使用 @ 將 e 綁定到 UnOp("abs", _),所以最終的返回結果是 UnOp(abs,Number(1.0))
Pattern guard
類似于 for() 中可以使用 if 守衛,case 后面也可以使用,例如我們期望將 x + x 變成 x * 2,按照上面的例子我們會這樣寫
scala> def simplifyAdd(e: Expr) = e match {
| case BinOp("+", x, x) => BinOp("*", x, Number(2))
| }
<console>:17: error: x is already defined as value x
case BinOp("+", x, x) => BinOp("*", x, Number(2))
^
但是報錯:x 已經被定義過了,因為同一個 x 不能出現兩次,解決方法是使用兩個變量 x 和 y,并判斷 x 是否等于 y:
scala> def simplifyAdd(e: Expr) = e match {
| case BinOp("+", x, y) if x==y => BinOp("*", x, Number(2))
| case _ => "something else"
| }
simplifyAdd: (e: Expr)java.io.Serializable
scala> simplifyAdd(BinOp("+", Number(1), Number(1)))
res1: java.io.Serializable = BinOp(*,Number(1.0),Number(2.0))
我們在 case 的后面加入了 if x==y,只有在滿足此條件才能匹配成功執行后面的操作,我們也可以匹配一個以 a 開頭的字符串:
scala> "apple" match{
| case s: String if s(0) == 'a' => "the " + s + " is start with a "
| case _ => "something else"
| }
res3: String = "the apple is start with a "
3、匹配的順序
接下來看一下 case 的書寫順序:
def simplifyAll(expr: Expr): Expr = expr match {
case UnOp("-", UnOp("-", UnOp("-", e))) => simplifyAll(e)
case BinOp("+", e, Number(0)) => simplifyAll(e)
case BinOp("*", e, Number(1)) => simplifyAll(e)
case UnOp(op, e) => UnOp(op, simplifyAll(e))
case BinOp(op, l, r) => BinOp(op, simplifyAll(l), simplifyAll(r))
case _ => expr
}
我們來看一下上面代碼中第 1 個 case 和第 4 個 case 的順序,因為第 1 個的匹配要比第 4 個的匹配更為嚴格,所以需要放在第 4 個的前面,否則永遠不可能匹配到第 1 個的情況:
scala> def simplifyBad(expr: Expr): Expr = expr match {
| case UnOp(op, e) => UnOp(op, simplifyBad(e))
| case UnOp("-", UnOp("-", e)) => simplifyBad(e)
| case _ => expr
| }
<console>:16: warning: unreachable code
case UnOp("-", UnOp("-", e)) => simplifyBad(e)
所以在書寫 case 時需要將更為嚴格的匹配放在前面
4、Sealed Class
如果想確保進行模式匹配的時候不會漏掉一些情況,可以使用 sealed 關鍵字修飾 class 如下所示:
sealed abstract class Expr
case class Var(name: String) extends Expr
case class Number(num: Double) extends Expr
case class UnOp(op: String, arg: Expr) extends Expr
case class BinOp(op: String, left: Expr, right: Expr) extends Expr
然后我們寫個函數測試一下:
scala> def describe(e: Expr): String = e match {
| case Number(_) => "a number"
| case Var(_) => "a variable"
| }
<console>:16: warning: match may not be exhaustive.
It would fail on the following inputs: BinOp(_, _, _), UnOp(_, _)
def describe(e: Expr): String = e match {
可見交互式終端剖出一個 warning 提示 匹配不是完全的,比如 BinOp(_, _, _), UnOp(_, _)
就不能匹配到,由此可以看出如果使用 sealed 關鍵字修飾類,那么進行模式匹配的時候,編譯器會檢查匹配的是否全面。
但是有的時候我們根據上下文可以確定只有上面代碼中寫到的兩種結果,這時可以在 e 的后面加上 : @unchecked 防止編譯器對其進行檢查
scala> def describe(e: Expr): String = (e: @unchecked) match {
| case Number(_) => "a number"
| case Var(_) => "a variable"
| }
describe: (e: Expr)String
可以看出這時就不會有 warning 了,這里的 @unchecked 是 Annotations(注釋),這時我們不做展開說明
5、Option 類型
Option 代表一個可選值,有兩種情況 Some(x) 代表有值,None 代表沒有找到對應值,這里我們以 Map 為例進行說明
scala> val writeCode = Map("Spark" -> "Scala", "Hadoop" -> "Java")
writeCode: scala.collection.immutable.Map[String,String] = Map(Spark -> Scala, Hadoop -> Java)
scala> writeCode.get("Spark")
res1: Option[String] = Some(Scala)
scala> writeCode.get("Kafka")
res2: Option[String] = None
使用 get 獲取 Spark 返回的是 Some(Scala) 說明 writeCode 中有這個值,而 Kafka 不再里面,所以返回的是 None
最常用的方法就是通過模式匹配來獲得可選值,示例如下:
scala> def show(x: Option[String]) = x match {
| case Some(s) => s
| case None => "?"
| }
show: (x: Option[String])String
scala> show(writeCode.get("Hadoop"))
res3: String = Java
scala> show(writeCode.get("Kafka"))
res4: String = ?
6、隨處可見的模式匹配
可以一次定義多個變量
例如:
scala> val (number, string) = (1, "abc")
number: Int = 1
string: String = abc
常用來接收函數的返回值,例如 Spark 源碼 SparkContext 中的如下部分
val (sched, ts) = SparkContext.createTaskScheduler(this, master)
同樣可以這樣使用:
scala> val BinOp(op, left, right) = BinOp("+", Number(1), Number(2))
op: String = +
left: Expr = Number(1.0)
right: Expr = Number(2.0)
一系列的 case 可以作為函數的一部分
例如:
scala> val withDefault: Option[Int] => Int = {
| case Some(x) => x
| case None => 0
| }
withDefault: Option[Int] => Int = <function1>
scala> withDefault(Some(100))
res7: Int = 100
scala> withDefault(None)
res8: Int = 0
可以看出 case 后面的部分其實是作為函數 withDefault 的函數體,這種寫法在消息通信中非常有用,例如 Spark 源碼 Worker 中的如下部分:
override def receive: PartialFunction[Any, Unit] = synchronized {
case SendHeartbeat =>
if (connected) { sendToMaster(Heartbeat(workerId, self)) }
case WorkDirCleanup =>
...
Worker 接收到消息后會判斷是什么消息,然后針對每種消息進行具體的操作
for 語句中使用模式匹配
例如我們要遍歷上面例子中的 Map 類型的 writeCode,代碼如下:
scala> for( (architecture, writeCode) <- writeCode )
| println("Architecture: " + architecture + " writeCode: " + writeCode)
Architecture: Spark writeCode: Scala
Architecture: Hadoop writeCode: Java
再來看另外一個例子:
scala> val results = List(Some("apple"), None, Some("orange"))
results: List[Option[String]] = List(Some(apple), None, Some(orange))
scala> for( Some(fruit) <- results) println(fruit)
apple
orange
總結
本文詳細闡述了 Scala 中的 case class 和 pattern matching,使我們更加便捷的編寫代碼,但是 Scala 中的模式匹配遠遠不止這么簡單,需要了解的可以研究 Scala 中的 Extractors。
本文參照:Programming in Scala, 3rd Edition 中的 Chapter 15:Case Class and Pattern Matching