Scala樣本類

博觀而約取,厚積而薄發(fā)夏志。

Scala樣本類

本章將重點介紹樣本類(case class)俭驮,及其在模式匹配(Pattern Matching)中的工作機制,及其具體運用啤贩。

JSON遞歸結(jié)構(gòu)

JSON(JavaScript Object Notation)是一種輕量級的數(shù)據(jù)交換格式。使用Scala拜秧,可以很容易地實現(xiàn)JSON遞歸結(jié)構(gòu)的定義痹屹。

JsValue的遞歸結(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的結(jié)構(gòu)

字符串化一個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的結(jié)構(gòu)

字符串化一個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類型蠢护。例如雅宾,JsStringunapply方法實現(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的類型為JsStringvalue的類型為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)]。則在使用模式匹配時祟辟,都可以使用中綴表示提取t1t2的值医瘫。

例如,在標(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)用Listinit, last都將耗費O(n)的時間復(fù)雜度陡蝇,顯然上述實現(xiàn)效率是非常低下的痊臭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市登夫,隨后出現(xiàn)的幾起案子广匙,更是在濱河造成了極大的恐慌,老刑警劉巖恼策,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸦致,死亡現(xiàn)場離奇詭異,居然都是意外死亡涣楷,警方通過查閱死者的電腦和手機分唾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狮斗,“玉大人绽乔,你說我怎么就攤上這事√及” “怎么了迄汛?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長骤视。 經(jīng)常有香客問我,道長鹃觉,這世上最難降的妖魔是什么专酗? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮盗扇,結(jié)果婚禮上祷肯,老公的妹妹穿的比我還像新娘沉填。我一直安慰自己,他們只是感情好佑笋,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布翼闹。 她就那樣靜靜地躺著,像睡著了一般蒋纬。 火紅的嫁衣襯著肌膚如雪猎荠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天蜀备,我揣著相機與錄音关摇,去河邊找鬼。 笑死碾阁,一個胖子當(dāng)著我的面吹牛输虱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脂凶,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼宪睹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蚕钦?” 一聲冷哼從身側(cè)響起亭病,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎冠桃,沒想到半個月后命贴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡食听,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年胸蛛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片樱报。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡葬项,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迹蛤,到底是詐尸還是另有隱情民珍,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布盗飒,位于F島的核電站嚷量,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逆趣。R本人自食惡果不足惜蝶溶,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抖所,春花似錦梨州、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至傻粘,卻和暖如春每窖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抹腿。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工岛请, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人警绩。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓崇败,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肩祥。 傳聞我的和親對象是個殘疾皇子后室,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 變量初始化可以用用 _ 作占位符,賦值為默認(rèn)值混狠,字符串 null岸霹,F(xiàn)loat、Int将饺、Double 等為 0var...
    FaDeo_O閱讀 915評論 0 0
  • java筆記第一天 == 和 equals ==比較的比較的是兩個變量的值是否相等贡避,對于引用型變量表示的是兩個變量...
    jmychou閱讀 1,497評論 0 3
  • 本文由我們團隊的 糾結(jié)倫 童鞋撰寫。 寫在前面 本篇文章是對我一次組內(nèi)分享的整理予弧,大部分圖片都是直接從keynot...
    知識小集閱讀 15,242評論 11 172
  • 也曾同數(shù)堂前落花 也曾共賞西天晚霞 最終選擇浪跡天涯 帶上你的遺憾出發(fā) 也曾問...
    明月牽你閱讀 273評論 1 4
  • 我十分驚訝,連忙問道:“怎么會蚓庭?致讥!” 璐璐說道:“被錄取的那人是公司老總的女兒。夕夕醬器赞,我突然覺得我的努力都是徒勞...
    夕夕醬閱讀 1,242評論 0 3