博觀而約取,厚積而薄發(fā)夏志。
Scala樣本類
本章將重點介紹樣本類(case class
)俭驮,及其在模式匹配(Pattern Matching
)中的工作機制,及其具體運用啤贩。
JSON遞歸結(jié)構(gòu)
JSON(JavaScript Object Notation)
是一種輕量級的數(shù)據(jù)交換格式。使用Scala
拜秧,可以很容易地實現(xiàn)JSON
遞歸結(jié)構(gòu)的定義痹屹。
sealed trait JsValue
case class JsBoolean(value: Boolean) extends JsValue
case class JsString(value: String) extends JsValue
case class JsNumber(value: BigDecimal) extends JsValue
case class JsArray(value: List[JsValue] = Nil) extends JsValue
case class JsObject(value: Map[JsString, JsValue]) extends JsValue
case object JsNull extends JsValue
樣本類
「樣本類」常常用于描述「不可變」的「值對象」(Value Object
)。樣本類的實現(xiàn)模式相當(dāng)簡單枉氮,它不僅消除了大量的樣板代碼志衍,而且在模式匹配中扮演了重要角色。
例如聊替,樣本類JsBoolean
是一個持有Boolean
值的JSON
對象楼肪。
case class JsBoolean(value: Boolean)
自動規(guī)則
一旦定義了樣本類,將免費得到很多特性佃牛。
- 隱式地聲明字段為
val
淹辞; - 自動混入特質(zhì)
ProductN
; - 自動地生成
equals, canEqual, hashCode, toString, copy
等方法俘侠; - 伴生對象中自動生成
apply, unapply
方法象缀。
揭秘樣本類
以樣本類JsBoolean
為例,它等價于如下實現(xiàn):
class JsBoolean(val value: Boolean) extends Product1[Boolean]
override def equals(obj: Any): Boolean = other match {
case other: JsBoolean => value == other.value
case _ => false
}
override def hashCode: Int = value.hashCode
override def toString: String = s"JsBoolean($value)"
def canEqual(other: Object): Boolean = other.isInstanceOf[JsBoolean]
def copy(value: Boolean): JsBoolean = new JsBoolean(value)
}
object JsBoolean {
def apply(value: Boolean) = new JsBoolean(value)
def unapply(b: JsBoolean): Option[Boolean] =
if (b != null) Some(b.value) else None
}
得與失
剖析樣本類JsBoolean
發(fā)現(xiàn)爷速,定義樣本類是非常簡單的央星,而且可以免費得到很多方法實現(xiàn)。唯一的副作用就是惫东,樣本類擴大了類的空間及其對象的大小莉给。
例如毙石,對于JsBoolean
可以進(jìn)行如下改進(jìn)。相對于樣本類的實現(xiàn)颓遏,實現(xiàn)了對象的共享徐矩,提高了效率。但是叁幢,代碼實現(xiàn)就沒有樣本類那么簡潔了滤灯。
sealed abstract class JsBoolean(val value: Boolean) extends JsValue
case object JsTrue extends JsBoolean(true)
case object JsFalse extends JsBoolean(false)
object JsBoolean {
def apply(value: Boolean) =
if (value) JsTrue else JsFalse
def unapply(b: JsBoolean): Option[Boolean] =
if (b != null) Some(b.value) else None
}
工廠方法
樣本類在伴生對象中自動地生成了apply
的工廠方法。在構(gòu)造樣本類的對象時曼玩,可以略去new
關(guān)鍵字鳞骤,言簡意賅,提高了代碼的表達(dá)力黍判。例如:
val capitals = JsObject(Map(
JsString("China") -> JsString("Beijing"),
JsString("France") -> JsString("Paris"),
JsString("US") -> JsString("Washington")))
析取器
定義了一個show
方法豫尽,它遞歸地將JsValue
轉(zhuǎn)換為字符串表示。
def show(json: JsValue): String = json match {
case JsArray(elems) => showArray(elems)
case JsObject(value) => showObject(value)
case JsString(str) => str
case JsNumber(num) => num.toString
case JsBoolean(bool) => bool.toString
case JsNull => "null"
}
其中顷帖,JsArray
是一個JsValue
的數(shù)組美旧,它可以用下圖描述:
字符串化一個JsArray
對象可以如下實現(xiàn):
private def showArray(values: List[JsValue]): String =
"[" + (values map show mkString ",") + "]"
其中,它等價于:
private def showArray(values: List[JsValue]): String =
"[" + (values.map(show).mkString(",")) + "]"
而JsObject
是一個包含Map[String, JsValue]
的對象窟她,它可以用下圖描述:
字符串化一個JsObject
對象可以如下實現(xiàn):
private def showObject(bindings: Map[JsString, JsValue]): String = {
val pairs = bindings map {
case (key, value) => s""""${show(key)}":${show(value)}"""
}
s"""{${(pairs mkString ",")}}"""
}
當(dāng)對樣本類進(jìn)行模式匹配時陈症,將調(diào)用伴生對象的unapply
方法。當(dāng)匹配成功后震糖,它返回一個使用Some
包裝的結(jié)果,然后被析取到相應(yīng)的變量之中去趴腋。
因此吊说,為了理解樣本類模式匹配的過程,必須先透徹理解unapply
的工作機制优炬。
單值析取器
JsValue
的所有樣本子類颁井,都是單值的樣本類。其unapply
方法將返回單值的Option
類型蠢护。例如雅宾,JsString
的unapply
方法實現(xiàn)如下。
object JsString {
def unapply(s: JsString): Option[String] =
if (s != null) Some(s.value) else None
}
當(dāng)對case JsString(value) => value
進(jìn)行模式匹配時葵硕,首先調(diào)用其伴生對象的unapply
方法眉抬。當(dāng)匹配成功后,返回Some
包裝的結(jié)果懈凹,最后被析取到value
的變量之中去了蜀变。
變量定義
也就是說,當(dāng)對樣本類JsString
的構(gòu)造參數(shù)進(jìn)行模式匹配時介评,其類似于發(fā)生如下的賦值過程库北,它將根據(jù)右邊的值爬舰,自動提取出capital
的值。
val JsString(capital) = JsString("Washington")
事實上寒瓦,上述形式的變量定義是模式匹配的典型應(yīng)用場景情屹。
迭代Map
再將目光投放回對JsObject
字符串化的過程。因為此處map
接受一個(JsString, JsValue) => String
類型的回調(diào)函數(shù)杂腰,應(yīng)此實現(xiàn)可以等價變換為:
def showObject(bindings: Map[JsString, JsValue]): String = {
val pairs = bindings map {
binding => s""""${show(binding._1)}":${show(binding._2)}"""
}
s"""{${(pairs mkString ",")}}"""
}
事實上垃你,回調(diào)的binding
類型為一個二元組,它的類型為(JsString, JsValue)
颈墅±猓可以通過調(diào)用_1, _2
方法分別提取出二元組的第1
個和第2
個元素的值,即Map
元素的鍵和值恤筛。
但是官还,調(diào)用_1, _2
方法,實現(xiàn)也顯得較為復(fù)雜毒坛,語義不太明確望伦。接下來嘗試提取「有名變量」的重構(gòu)手法,改善代碼的表現(xiàn)力煎殷。
for推導(dǎo)式
首先屯伞,嘗試使用for
推導(dǎo)式,可以得到等價的重構(gòu)效果豪直。
def showObject(bindings: Map[JsString, JsValue]): String = {
val pairs = for ((key, value) <- bindings)
yield s""""${show(key)}":${show(value)}"""
s"""{${(pairs mkString ",")}}"""
}
此處劣摇,(key, value) <- bindings
直接析取出Map
的鍵值對,避免了_1, _2
的神秘調(diào)用弓乙;其中末融,key
的類型為JsString
,value
的類型為JsValue
暇韧。
偏函數(shù)
其次勾习,也可以使用偏函數(shù),直接獲取出Map
的鍵值對懈玻。
def showObject(bindings: Map[JsString, JsValue]): String = {
val pairs = bindings map {
case (key, value) => s""""${show(key)}":${show(value)}"""
}
s"""{${(pairs mkString ",")}}"""
}
此處巧婶,傳遞給map
的實際上是一個「偏函數(shù)」,它的類型為:PartialFunction[(JsString, JsValue), String]
涂乌。
當(dāng)模式匹配成功后艺栈,它直接析取出(key, value)
的值;其中骂倘,key
的類型為JsString
眼滤,value
的類型為JsValue
。
也可以對上述實現(xiàn)進(jìn)行局部重構(gòu)历涝,凸顯偏函數(shù)的類型信息诅需。
def showObject(bindings: Map[JsString, JsValue]): String = {
val f: PartialFunction[(JsString, JsValue), String] = {
case (key, value) => s""""${show(key)}":${show(value)}"""
}
s"""{${(bindings map f mkString ",")}}"""
}
因為PartialFunction[-T, +R]
是T => R
的子類型漾唉,上述實現(xiàn)也可以重構(gòu)為:
def showObject(bindings: Map[JsString, JsValue]): String = {
val f: (JsString, JsValue) => String = {
case (key, value) => s""""${show(key)}":${show(value)}"""
}
s"""{${(bindings map f mkString ",")}}"""
}
多值析取器
對于for
推導(dǎo)式,及其應(yīng)用偏函數(shù)堰塌。例如赵刑,對于偏函數(shù)f
,是如何析取鍵值對(key, value)
的值呢场刑?
val f: PartialFunction[(JsString, JsValue), String] = {
case (key, value) => s""""${show(key)}":${show(value)}"""
}
事實上般此,該偏函數(shù)背后由Tuple2
支撐完成工作的。首先牵现,Tuple2
大致如下定義:
case class Tuple2[+T1, +T2](_1: T1, _2: T2)
在合成的伴生對象中铐懊,unapply
方法大致如下實現(xiàn):
object Tuple2 {
def unapply[T1, T2](t: Tuple2[T1, T2]): Option[Tuple2[T1, T2]] =
if (t != null) Some(t._1 -> t._2) else None
}
當(dāng)它模式匹配成功后,isDefinedAt
返回true
瞎疼;然后調(diào)用Tuple2
伴生對象的unapply
方法返回Some(t._1 -> t._2)
的值科乎,最后將t._1, t._2
的值分別賦予(key, value)
。
形式化
綜上述贼急,可以得到apply茅茂,unapply
的一般規(guī)則,并可以進(jìn)行形式化地描述太抓。一般地空闲,對于任意的樣本類CaseObject
,其擁有T1, T2, ..., Tn
構(gòu)造參數(shù)走敌。
case class CaseObject[+T1, +T2, ..., +Tn](t1: T1, t2: T2, ..., tn: Tn)
其伴生對象中的apply, unapply
方法將存在如下的實現(xiàn)碴倾。
object CaseObject {
def apply[T1, T2, ..., Tn](
t1: T1, t2: T2, ..., tn: Tn) = new CaseObject(t1, t2, ..., tn)
def unapply[T1, T2, ..., Tn](
o: CaseObject[T1, T2, ..., Tn]): Option[(T1, T2, ..., Tn)] =
if (o != null) Some(o.t1, o.t2, ..., o.tn) else None
}
當(dāng)模式匹配成功,它將返回Some[T1, T2, ..., Tn]
類型的結(jié)果掉丽;否則返回None
影斑。
當(dāng) n == 1
特殊地,當(dāng)n == 1
机打,對于任意的樣本類Unary[+T]
,其擁有一個構(gòu)造參數(shù)片迅。
case class Unary[+T](t: T)
因為不存在單值的元組類型残邀,因此其伴生對象中合成的unapply
將直接返回Option[T]
。
object Unary {
def apply[T](t: T): Unary[T] = new Unary(t)
def unapply[T](o: Unary[T]): Option[T] =
if (o != null) Some(o.t) else None
}
當(dāng) n == 2
特殊地柑蛇,當(dāng)n == 2
芥挣,對于任意的樣本類Binary[+T1, +T2]
,其擁有兩個構(gòu)造參數(shù)耻台。
case class Binary[+T1, +T2](t1: T1, t2: T2)
在其合成的伴生對象中空免,unapply
的返回值類型為Option[(T1, T2)]
。
object Binary {
def apply[T1, T2](t1: T1, t2: T2): Binary[T1, T2] =
new Binary(t1, t2)
def unapply[T1, T2](o: Binary[T1, T2]): Option[(T1, T2)] =
if (o != null) Some(o.t1 -> o.t2) else None
}
特殊地盆耽,對于二元的樣本類蹋砚,當(dāng)它被應(yīng)用于模式匹配時扼菠,case
表達(dá)式可以使用中綴表示。
def f[T1, T2, R]: Binary[T1, T2] => R = {
case t1 Binary t2 => ???
}
中綴表達(dá)式
例如坝咐,對于樣本類Tuple2
循榆,其伴生對象中合成的unapply
方法就是返回了一個Option
修飾的二元組。
因此墨坚,上例的showObject
定義的偏函數(shù)f
也可以變換為:
val f: Tuple2[JsString, JsValue] => String = {
case key Tuple2 value => s""""${show(key)}":${show(value)}"""
}
對于Tuple2
秧饮,中綴表示的可讀性顯然不佳。但是泽篮,Tuple2[T1, T2]
可以簡化為(T1, T2)
的語法糖表示盗尸,因此上例可以等價轉(zhuǎn)換為:
val f: (JsString, JsValue) => String = {
case (key, value) => s""""${show(key)}":${show(value)}"""
}
但是,但對于使用“操作符”命名的樣本類帽撑,使用中綴表示的case
表達(dá)式會極大地改善代碼的可讀性泼各。
接下來,以List
的非空節(jié)點::
油狂,及其單鍵對象+:, :+
為例历恐,講解中綴表示在case
表達(dá)式中原理與運用。
樣本類:::
首先专筷,List
是一個遞歸的數(shù)據(jù)結(jié)構(gòu)弱贼。其中,對于非空節(jié)點::
磷蛹,它并非操作符吮旅,而是一個類名。
sealed trait List[+A]
case class ::[A](head: A, tail: List[A]) extends List[A]
case object Nil extends List[Nothing]
事實上味咳,這樣的命名方式是有特殊意圖的庇勃。剖析case class ::
,它存在如下部分實現(xiàn)槽驶。
class ::[A](val head: A, val tail: List[A])
object :: {
def apply[A](head: A, tail: List[A]) =
new ::(head, tail)
def unapply[A](l: ::[A]): Option[(A, List[A])] =
if (l.isEmpty) None else Some(l.head -> l.tail)
}
因為伴生對象::
的unapply
方法返回一個二元組责嚷;當(dāng)使用模式匹配時,case
表達(dá)式可以使用中綴表示掂铐。例如罕拂,count
方法用于計算滿足謂詞的列表元素的數(shù)目。
def count[A](l: List[A])(p: A => Boolean): Int = l match {
case head :: tail if(p(head)) => 1 + count(tail)(p)
case _ :: tail => count(tail)(p)
case _ => 0
}
中綴表示鮮明地描述了List
的特征全陨,case
表達(dá)式很形象地表達(dá)了析取List
的「頭節(jié)點」和「尾列表」的意圖爆班,具有很強的表達(dá)力。
事實上辱姨,它等價于如下的實現(xiàn)柿菩,但表達(dá)力顯然不如前者。
def count[A](l: List[A])(p: A => Boolean): Int = l match {
case ::(head, tail) if(p(head)) => 1 + count(tail)(p)
case ::(_, tail) => count(tail)(p)
case _ => 0
}
單鍵對象:+:與:+
事實上雨涛,對于任何的單鍵對象op
枢舶,只要其unapply
能夠?qū)⑷萜?code>C進(jìn)行解構(gòu)懦胞,并返回Option[(T1, T2)]
。則在使用模式匹配時祟辟,都可以使用中綴表示提取t1
與t2
的值医瘫。
例如,在標(biāo)準(zhǔn)庫中存在一個單鍵對象+:
旧困,它的功能類似于伴生對象::
醇份,用于將SeqLike
類型的集合中析取出「頭節(jié)點」和「尾序列」。
object +: {
def unapply[T, C <: SeqLike[T, C]](
c: C with SeqLike[T, C]): Option[(T, C)] =
if(c.isEmpty) None else Some(c.head -> c.tail)
}
例如吼具,上述count
也可以實現(xiàn)為:
def count[A](l: List[A])(p: A => Boolean): Int = l match {
case head +: tail if(p(head)) => 1 + count(tail)(p)
case _ +: tail => count(tail)(p)
case _ => 0
}
同樣地僚纷,標(biāo)準(zhǔn)庫中也存在另一個單鍵對象:+
,它的功能與+:
相反拗盒,它用于析取集合中的「頭列表」和「尾節(jié)點」怖竭。
object :+ {
def unapply[T, C <: SeqLike[T, C]](
c: C with SeqLike[T, C]): Option[(C, T)] =
if(c.isEmpty) None else Some(c.init -> c.last)
}
例如,上述count
也可以實現(xiàn)為:
def count[A](l: List[A])(p: A => Boolean): Int = l match {
case init :+ last if(p(last)) => count(init)(p) + 1
case init :+ _ => count(init)(p)
case _ => 0
}
因為調(diào)用List
的init, last
都將耗費O(n)
的時間復(fù)雜度陡蝇,顯然上述實現(xiàn)效率是非常低下的痊臭。