基本模式


基本模式可以單獨運用,也可以彼此組合,以形成更複雜的模式。這將將介紹的基本模式包括了:
  • 常數模式(Constant pattern)
  • 萬用字元模式(Wildcard pattern)
  • 建構式模式(Constructor pattern)
  • 變數模式(Variable pattern)
  • 型別模式(Typed pattern)

模式中最簡單的種類是常數模式(Constant pattern),你可以在Scala中寫下的字面常量(Literal)都可以作為模式比對,例如:
def what(a: Any) = a match {
case 10 => "整數"
case 0.1 => "浮點數"
case 'A' => "字元"
case true => "布林值"
case "text" => "字串"
case Nil => "空串列"
case _ => "?"
}

使用match運算式時,如果無法比對成功,會丟出MatchError。你可以在最後的case放一個_,這表示符合任何對象,這是萬用字元模式(Wildcard pattern)的一個應用。以下也是個萬用字元模式的運用,你只想知道傳入的是Point或不是Point:
case class Point(x: Int, y: Int)

def what(a: Any) = a match {
case Point(_, _) => "圓"
case _ => "不是圓"
}

println(what(Point(1, 2))) // 顯示圓
println(what(Point(3, 4))) // 顯示圓
println(what("圓?")) // 顯示不是圓

實際上,上面先使用建構式模式(Constructor pattern),看看傳入的物件是不是Point所建構,如果是的話,再進一步來到了萬用字元模式,所以不在乎x或y值為何。

來看看變數模式(Variable pattern)運用的一個例子:
def what(i: Any) = i match {
case 100 => "滿分"
case 90 => "A"
case something => "不及格?" + something
}

println(what(100)) // 滿分
println(what(90)) // A
println(what(80)) // 不及格?80
 
在不是100或90的情況下,則符合最後的case,而且會將比對的物件指定給something這個變數。這個例子看不出變數模式的實際運用,來看看這個例子:
case class Point(x: Int, y: Int)

def what(a: Any) = a match {
case Point(x, y) => "圓 (" + x + ", " + y + ")"
case _ => "不是圓"
}

println(what(Point(1, 2))) // 圓 (1, 2)
println(what(Point(3, 4))) // 圓 (3, 4)
println(what("圓?")) // 不是圓

上面先使用建構式模式(Constructor pattern),看看傳入的物件是不是Point所建構,如果是的話,再進一步將Point中的值分別指定給x與y變數,在=>之中就可以直接取用x與y的值。

在使用常數模式時,需注意別與變數模式混淆,例如你也許以為下面這個程式是常數模式比對:
val x = 10
def what(i: Int) = i match {
case x => "10"
case _ => "不是 10"
}

println(what(10))
println(what(20))

但事實是,你使用了變數模式,x是match中的一個變數,而不是你在第一行所宣告的x,上面的程式會編譯錯誤:
error: unreachable code
    case _   => "不是 10"
                 ^

變數模式一定會先匹配到,所以之後的萬用字元模式永遠不會被匹配到。在Scala中,一個常數在命名時,首字母必須大寫,這不僅是慣例,也是在某些場合被認定為常數的要件。例如以下的程式就可以執行:
val X = 10
def what(i: Int) = i match {
case X => "10"
case _ => "不是 10"
}
println(what(10)) // 10
println(what(20)) // 不是 10

變數X是首字大寫,在match中會被認定為常數模式,因此可以編譯成功並執行。

再繼續來看到建構式模式,它可以形成巢狀,例如:
case class Point(x: Int, y: Int)
case class Circle(p: Point, r: Int)
case class Cylinder(c: Circle, h: Int)

def what(a: Any) = a match {
case Point(_, _) => "點"
case Circle(Point(_, _), _) => "圓"
case Cylinder(Circle(Point(_, _), _), _) => "柱"
}

println(what(Point(10, 10))) // 點
println(what(Circle(Point(10, 10), 10))) // 圓
println(what(Cylinder(Circle(Point(10, 10), 10), 10))) // 柱

上例中使用了建構式模式與萬用字元模式,以傳入Cylinder為例,會使用建構式模式比對Cylinder,符合後再使用建構式模式比較內層的Circle,符合後再使用建構式比較更內層的Point,最後使用萬用字元比對。

再來看到型別模式(Typed pattern),直接使用 重新定義 equals() 方法 中的一個例子作說明:
class Point(val x: Int, val y: Int) {
override def equals(a: Any) = a match {
case that: Point => this.x == that.x && this.y == that.y
case _ => false
}
override def hashCode = 41 * (41 + x) + y
}

在第一個case中的比對中,傳入的物件型態必須符合Point型別,如果是的話,指定給that變數。這個例子如果不使用型別模式,則你可以這麼撰寫:
class Point(val x: Int, val y: Int) {
override def equals(a: Any) = {
if(a.isInstanceOf[Point]) {
val that = a.asInstanceOf[Point]
this.x == that.x && this.y == that.y
}
false
}

override def hashCode = 41 * (41 + x) + y
}

一般來說,不鼓勵直接進行型態檢查與型態轉換,寫來也比較冗長,建議還是採用模式匹配的方式。

在使用型別模式時,若想匹配List、Set、Map等型態,可以使用以下的方式:
def what(a: Any) = a match {
case str : String => "字串"
case list: List[_] => "串列"
case set : Set[_] => "集合"
case map : Map[_, _] => "字典"
case _ => "別的東西"
}

println(what("text")) // 字串
println(what(List(1, 2))) // 串列
println(what(Set(1, 2, 3))) // 集合

但是你沒辦法指匹配群集中別元素型態,例如:
def what(a: Any) = a match {
case list: List[String] => "字串串列"
case _ => "別的東西"
}

這在編譯時會出現警示訊息:
warning: there were unchecked warnings; re-run with -unchecked for details

你可以在編譯時加以-unchecked引數,看到詳細的警示訊息:
warning: non variable type-argument String in type pattern is unchecked since it is eliminated by erasure
    case list: List[String]  => "字串串列"
                ^

理由在於,Scala的泛型(Generic)採用的是型別抹除(Type erasure)的作法,加入群集後的物件基本上就失去型態資訊了(如果你熟悉Java,這跟物件加入Java群集中意思是一樣的,所有的物件失去的型態資訊)。如果你執意運行以下的程式,結果將不正確:
def what(a: Any) = a match {
case list: List[String] => "字串串列"
case _ => "別的東西"
}
val list1 = List("text")
val list2 = List(1)
println(what(list1)) // 字串串列
println(what(list2)) // 字串串列

唯一的例外是陣列,陣列沒有採用型別抹除,因為陣列在Scala中處理的方式特意與Java中陣列相同,所以下面的寫法是可行的:
def what(a: Any) = a match {
case arr: Array[Int] => "整數陣列"
case arr: Array[String] => "字串陣列"
case _ => "別的東西"
}

val arr1 = Array(1)
val arr2 = Array("text")
println(what(arr1)) // 整數陣列
println(what(arr2)) // 字串陣列