看完最直接的pattern match基礎應用,稍微講一些周邊作為番外的Ending吧,常常與pattern match一起提到的概念大概有Extrator、Case Class、Sealed Class、Enum、Option等等。來看看到底是啥回事吧~
[Snippet.55] patten with Regex
開始之前,先來個patten match小複習。
patten match跟還能搭配Regex,如何!
val pattern = "([0-9]+) ([a-z]+)".r ①
"99 bottles" match {
case pattern(num, item) => println("num=" + num + ", and item =" + item) ②
case _ => "anything else"
}
①在Scala中怎麼宣告一個Regex物件勒?String.r
就是這麼簡單
②抽出兩個比對到的元素,分別放入num
跟item
內
如果仔細想一下,就會覺得昨天那些集合的match很厲害,為啥case Array(0, z)
、case x :: y => x + " " + y
、case (_, 0) => "..0"
可以抓出值來配對勒??因為有所謂的Extractor
。Extractor的簡單定義是Object with an unapply method
,舉個簡單的範例吧:
object Twice ①{
def apply(x: Int): Int = x
def unapply(z: Int): Option[Int] = ②
if (z % 2 == 0) Some(z / 2) else Some(100)
}
①將Twice宣告為object
(singleton)
②建立unapply
methodapply
的概念是將參數帶入以產生物件,而unapply
當然就是將物件帶入,抽出值啦~
有了Twice物件後,寫個match吧:
def twiceMatch(twice: Any): Any =
twice match {
case Twice(n) => n
}
twiceMatch(Twice(20))
twiceMatch(Twice(21))
這樣輸出是啥勒?
//Output:
10
100
答對了嗎?
case居然能變成class!!!XDD 別懷疑,直接上官方文件的說明吧!
Scala supports the notion of case classes. Case classes are just regular classes that are:
- Immutable by default
- Decomposable through pattern matching ①
- Compared by structural equality instead of by reference ②
- Succinct to instantiate and operate on
①就代表case Class已經自帶apply, unapply了啦,不用自己寫
②equals、hashCode也做好了可以直接比較
另外case Class也實作了簡單的toString
,因為是immutable的,所以還有做一個copy
方便複製,等等來玩玩。除此之外,與一般Class無異,他可以繼承也可以被繼承。
我的感覺是Case Class跟Java POJO有點像,這類class都不會太複雜,拿來封裝比較用的。
先寫幾個簡易的 Case Class玩玩吧:
[Snippet.56] Case Class
abstract class Amount ①
case class Dollar(value: Double) extends Amount ②
case class Currency(value: Double, unit: String) extends Amount ③
case object Nothing extends Amount ④
①一個名為Amount的Abstract Class
②繼承Amount的Case Class,宣告方式跟一般Scala Class差不多,也能繼承別人~
③另外一個Case Class
④Case Class也能宣告成Object
建好後來玩玩吧:
object AmountTest extends App {
val currency = Currency(20, "NT") ①
println(currency.value)
println(currency.unit)
//immutable
//currency.unit = "US"; ②
val anotherDollar = currency.copy(unit = "US") ③
println(anotherDollar.value)
def amtMatch(amt: Amount): Any = ④
amt match {
case Dollar(value) => "$" + value ⑤
case Currency(value, unit) => value + ", and unit is " + unit ⑥
case Nothing => ""
}
println(amtMatch(Dollar(20)))
println(amtMatch(Currency(30, "NT")))
println(amtMatch(Nothing))
}
①宣告一個Currency的Case Class,並且印出他的field member
②這些field都是val(immutable),如果修改會報Error喔(reassignment to value)
③如果我只想小修一個欄位,例如換掉Currency的單位勒,你可以透過copy
方式來修改
④進入pattern match吧!
⑤ 此時Case的對象就是case Class的物件囉,可以寫成Dollar(value)
跟Currency(value, unit)
因為Case Class已經內建unapply啦~
輸出:
20.0
NT
20.0
$20.0
30.0, and unit is NT
之前在[Snippet.41]建立updateSateByKey時,func裏面有用到Some跟Null搭配pattern match還記得嗎?
def func(
vals: Seq[(Double)],preValue: Option[Double]): Option[Double] = ①
preValue match { ②
case Some(total) => Some(vals.sum + total) ③
case None => Some(vals.sum) ④
}
現在應該可以推測的出來Some跟None是啥了吧!沒錯,也是Case Class!
final case class Some[+A](x: A) extends Option[A] {
def isEmpty = false
def get = x
}
case object None extends Option[Nothing] {
def isEmpty = true
def get = throw new NoSuchElementException("None.get")
}
在deconstruct或是在用遞迴拆解List的時候,常常會冒出list.head::list.tail這樣的東西,為啥可以寫成這樣勒?::
又是啥阿喔~其實這一切都是infix的功能啦!
當一個case class有兩個parameter時
,你在pattern match中
可以使用infix的方式代表class
,也就是 paraA + ClassName + paraB這種乍看之下很奇怪,但有時候反而比較直覺的寫法,舉個例吧:
case class And(val val1: Int, val val2: Int) ①
object InfixTest extends App {
val obj = And(3, 4) ②
obj match {
case 3 And 4 => println("haha") ③
case _ => println("nothing")
}}
①宣告有兩個參數(parameter)的Case Class
②初始化一個Case Class物件取名為obj
③在patten match中用infix方法
表示obj
所以之前的amtMatch
也能用infix-style改寫Currency(因為他有兩個參數阿~)
def amtMatch2WithInfix(amt: Amount): Any =
amt match {
case Dollar(v) => "$" + v
case value Currency unit => value + ", and unit is " + unit ①
case Nothing => ""
}
①改成value在前,單位在後,ClassName居中的表示方式。
所以其實拆解List時用的::
根本就是一個Case Class阿XD好妙,看看原始碼吧!
final case class ::[B](override val head: B, private[scala] var tl: List[B]) ① extends List[B] {
override def tail : List[B] = tl
override def isEmpty: Boolean = false
}
看到①有沒有,參數就是一個泛型B的變數跟一個List[B],然後body中tl指給tail,所以就可以寫成head::tail啦。
有時候你在列舉case class怕會遺漏,可以在他的superclass上面注明sealed
,拿直接的範例來改就會變成:
sealed abstract class Amount ①
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
case class AnotherCurrency(value: Double, unit: String) extends Amount ②
case object Nothing extends Amount
①加上sealed標記的superClass,這樣要注意seal Class與其Case Class要在同一個檔案
中。這點比較特別
②額外亂加一個先前patten match中沒有的Case Class。
若重新compile則會跳出WARN:
Warning:(30, 5) match may not be exhaustive.
It would fail on the following input: AnotherCurrency(_, _)
amt match {
不想看到WARN可以在最後面加上_
或是@unchecked Annotation標記告知compiler。
Case Class還能幹嘛勒?還能拿來當作Enum使用!,例如:
sealed abstract class TrafficLight
case object Red extends TrafficLight
case object Yellow extends TrafficLight
case object Green extends TrafficLight
object EnumSimulationTest extends App {
val color: TrafficLight = Red
color match {
case Red => println("The color is red")
case Yellow => println("The color is yellow")
case Green => println("The color is Green")
}
}
當然Scala也有Enumeration helper可以用,看看一般的Enum怎麼寫吧:
object TrafficLight extends Enumeration {
type TrafficLight = Value
val Red = Value
val Yellow = Value(20, "red")
val Green = Value("green")
}
簡單說一下Enum,基本上每個Enum元素都由(ID,value)構成。
像Red
啥都沒寫,因為ID預設為0開始遞增,所以Red的ID為0。value沒宣告就是等於名稱(Red)。Yellow
的ID為20,值為"red"XD。Green
沒宣告ID只有宣告value,ID就會從前一個(Yellow.id)遞增1,所以是21。
可以用for express觀察
for (c <- TrafficLightColor.values) println(c.id + " : " + c)
//Output:
//0 : Red
//20 : red
//21 : green