Scala提取器

提取器

在之前的文章中寫過一個非常強大的語言特性: 模式匹配 袜炕。 它可以解綁一個給定的數據結構瞄桨。 這不是 Scala 所特有的遣疯,在其他出色的語言中,如 Haskell扶镀、Erlang蕴侣,模式匹配也扮演著重要的角色。

模式匹配可以解構各種數據結構臭觉,包括 列表 昆雀、 ,以及 樣例類 蝠筑。 但只有這些數據結構才能被解構嗎狞膘,還是可以用某種方式擴展其使用范圍? 而且什乙,它實際是怎么工作的客冈? 是不是有什么魔法在里面,得以寫些類似下面的代碼稳强?

 case class User(firstName: String, lastName: String, score: Int)
 def advance(xs: List[User]) = xs match {
   case User(_, _, score1) :: User(_, _, score2) :: _ => score1 - score2
   case _ => 0
 }

事實證明沒有什么魔法场仲,這都歸功于提取器

提取器使用最為廣泛的使用有著與 構造器 相反的效果: 構造器從給定的參數列表創(chuàng)建一個對象退疫, 而提取器卻是從傳遞給它的對象中提取出構造該對象的參數渠缕。 Scala 標準庫包含了一些預定義的提取器,我們會大致的了解一下它們褒繁。

樣例類非常特殊亦鳞,Scala會自動為其創(chuàng)建一個 伴生對象 : 一個包含了 applyunapply 方法的 單例對象apply 方法用來創(chuàng)建樣例類的實例棒坏,而 unapply 需要被伴生對象實現燕差,以使其成為提取器。

第一個提取器

unapply 方法可能不止有一種方法簽名坝冕, 不過徒探,我們從只有最簡單的開始,畢竟使用更廣泛的還是只有一種方法簽名的 unapply 喂窟。 假設要創(chuàng)建了一個 User 特質测暗,有兩個類繼承自它央串,并且包含一個字段:

trait User {
  def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User

我們想在各自的伴生對象中為 FreeUserPremiumUser 類實現提取器, 就像 Scala 為樣例類所做的一樣碗啄。 如果想讓樣例類只支持從給定對象中提取單個參數质和,那 unapply 方法的簽名看起來應該是這個樣子:

  def unapply(object: S): Option[T]

這個方法接受一個類型為 S 的對象,返回類型 TOption 稚字, T 就是要提取的參數類型饲宿。

在Scala中, Optionnull 值的安全替代胆描。 以后會有一個單獨的章節(jié)來講述它瘫想,不過現在,只需要知道袄友, unapply 方法要么返回 Some[T] (如果它能成功提取出參數)殿托,要么返回 NoneNone表示參數不能被 unapply 具體實現中的任一提取規(guī)則所提取出剧蚣。

下面的代碼是我們的提取器:

trait User {
  def name: String
}
class FreeUser(val name: String) extends User
class PremiumUser(val name: String) extends User
object FreeUser {
  def unapply(user: FreeUser): Option[String] = Some(user.name)
}
object PremiumUser {
  def unapply(user: PremiumUser): Option[String] = Some(user.name)
}

現在支竹,可以在REPL中使用它:

scala> FreeUser.unapply(new FreeUser("Daniel"))
res0: Option[String] = Some(Daniel)

如果調用返回的結果是 Some[T] ,說明提取模式匹配成功鸠按,如果是 None 礼搁,說明模式不匹配。

一般不會直接調用它目尖,因為用于提取器模式時馒吴,Scala 會隱式的調用提取器的 unapply 方法。

  val user: User = new PremiumUser("Daniel")
  user match {
    case FreeUser(name) => "Hello" + name
    case PremiumUser(name) => "Welcome back, dear" + name
  }

你會發(fā)現瑟曲,兩個提取器絕不會都返回 None 饮戳。 這個例子展示的提取器要比之前所見的更有意義。 如果你有一個類型不確定的對象洞拨,你可以同時檢查其類型并解構扯罐。

這個例子里, FreeUser 模式并不會匹配烦衣,因為它接受的類型和我們傳遞給它的不一樣歹河。 這樣一來, user 對象就會被傳遞給第二個模式花吟,也就是 PremiumUser 伴生對象的 unapply 方法秸歧。 而這個模式會匹配成功,從而返回值就被綁定到 name 參數上衅澈。

在接下來的文章里键菱,我們會看到一個并不總是返回 Some[T] 的提取器的例子。

提取多個值

現在矾麻,假設類有多個字段:

trait User {
  def name: String
  def score: Int
}
class FreeUser(
  val name: String,
  val score: Int,
  val upgradeProbability: Double
) extends User
class PremiumUser(
  val name: String,
  val score: Int
) extends User

如果提取器想解構出多個參數纱耻,那它的 unapply 方法應該有這樣的簽名:

def unapply(object: S): Option[(T1, ..., T2)]

這個方法接受類型為 S 的對象芭梯,返回類型參數為 TupleNOption 實例险耀, TupleN 中的 N 是要提取的參數個數弄喘。

修改類之后,提取器也要做相應的修改:

trait User {
  def name: String
  def score: Int
}
class FreeUser(
  val name: String,
  val score: Int,
  val upgradeProbability: Double
) extends User
class PremiumUser(
  val name: String,
  val score: Int
) extends User
object FreeUser {
  def unapply(user: FreeUser): Option[(String, Int, Double)] =
    Some((user.name, user.score, user.upgradeProbability))
}
object PremiumUser {
  def unapply(user: PremiumUser): Option[(String, Int)] =
    Some((user.name, user.score))
}

現在可以拿它來做模式匹配了:

val user: User = new FreeUser("Daniel", 3000, 0.7d)
user match {
  case FreeUser(name, _, p) =>
    if (p > 0.75) "$name, what can we do for you today?"
    else "Hello $name"
  case PremiumUser(name, _) =>
    "Welcome back, dear $name"
}

布爾提取器

有些時候甩牺,進行模式匹配并不是為了提取參數蘑志,而是為了檢查其是否匹配。 這種情況下贬派,第三種 unapply方法簽名(也是最后一種)就有用了急但, 這個方法接受 S 類型的對象,返回一個布爾值:

def unapply(object: S): Boolean

使用的時候搞乏,如果這個提取器返回 true 波桩,模式會匹配成功, 否則请敦,Scala 會嘗試拿 object 匹配下一個模式镐躲。

上一個例子存在一些邏輯代碼,用來檢查一個免費用戶有沒有可能被說服去升級他的賬戶侍筛。 其實可以把這個邏輯放在一個單獨的提取器中:

object premiumCandidate {
  def unapply(user: FreeUser): Boolean = user.upgradeProbability > 0.75
}

你會發(fā)現萤皂,提取器不一定非要在這個類的伴生對象中定義。 正如其定義一樣匣椰,這個提取器的使用方法也很簡單:

val user: User = new FreeUser("Daniel", 2500, 0.8d)
user match {
  case freeUser @ premiumCandidate() => initiateSpamProgram(freeUser)
  case _ => sendRegularNewsletter(user)
}

使用的時候裆熙,只需要把一個空的參數列表傳遞給提取器,因為它并不真的需要提取數據禽笑,自然也沒必要綁定變量入录。

這個例子有一個看起來比較奇怪的地方: 我假設存在一個空想的 initiateSpamProgram 函數,其接受一個 FreeUser 對象作為參數佳镜。 模式可以與任何一種 User 類型的實例進行匹配僚稿,但 initiateSpamProgram 不行, 只有將實例強制轉換為 FreeUser 類型邀杏, initiateSpamProgram 才能接受贫奠。

因為如此,Scala 的模式匹配也允許將提取器匹配成功的實例綁定到一個變量上望蜡, 這個變量有著與提取器所接受的對象相同的類型唤崭。這通過 @ 操作符實現。 premiumCandidate 接受 FreeUser 對象脖律,因此谢肾,變量 freeUser 的類型也就是 FreeUser

布爾提取器的使用并沒有那么頻繁(就我自己的情況來說)小泉,但知道它存在也是很好的芦疏, 或遲或早冕杠,你會遇到一個使用布爾提取器的場景。

中綴表達方式

解構列表酸茴、流的方法與創(chuàng)建它們的方法類似分预,都是使用 cons 操作符: ::#:: 薪捍,比如:

val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
  case first #:: second #:: _ => first - second
  case _ => -1
}

你可能會對這種做法產生困惑笼痹。 除了我們已經見過的提取器用法,Scala 還允許以中綴方式來使用提取器酪穿。 所以凳干,我們可以寫成 e(p1, p2) ,也可以寫成 p1 e p2 被济, 其中 e 是提取器救赐, p1p2 是要提取的參數只磷。

同樣经磅,中綴操作方式的 head #:: tail 可以被寫成 #::(head, tail) , 提取器 PremiumUser 可以這樣使用: name PremiumUser score 喳瓣。 當然馋贤,這樣做并沒有什么實踐意義。 一般來說畏陕,只有當一個提取器看起來真的像操作符配乓,才推薦以中綴操作方式來使用它。 所以惠毁,列表和流的 cons 操作符一般使用中綴表達犹芹,而 PreimumUser 則不用。

進一步看流提取器

盡管 #:: 提取器在模式匹配中的使用并沒有什么特殊的鞠绰, 但是腰埂,為了更好的理解上面的代碼,還是進一步來分析一下蜈膨。 而且屿笼,這是一個很好的例子,根據要匹配的數據結構的狀態(tài)翁巍,提取器很可能返回 None 驴一。

如下是 Scala 2.9.2 源代碼中完整的 #:: 提取器代碼:

object #:: {
  def unapply[A](xs: Stream[A]): Option[(A, Stream[A]) =
    if (xs.isEmpty) None
    else Some((xs.head, xs.tail))
}

如果給定的流是空的,提取器就直接返回 None 灶壶。 因此肝断, case head #:: tail 就不會匹配任何空的流。 否則,就會返回一個 Tuple2 胸懈,其第一個元素是流的頭担扑,第二個元素是流的尾,尾本身又是一個流趣钱。 這樣涌献, case head #:: tail 就會匹配有一個或多個元素的流。 如果只有一個元素羔挡, tail 就會被綁定成空流洁奈。

為了理解流提取器是怎么在模式匹配中工作的间唉,重寫上面的例子绞灼,把它從中綴寫法轉成普通的提取器模式寫法:

val xs = 58 #:: 43 #:: 93 #:: Stream.empty
xs match {
  case #::(first, #::(second, _)) => first - second
  case _ => -1
}

首先為傳遞給模式匹配的初始流 xs 調用提取器。 由于提取器返回 Some(xs.head, xs.tail) 呈野,從而 first 會綁定成 58低矮, xs 的尾會繼續(xù)傳遞給提取器,提取器再一次被調用被冒,返回首和尾军掂, second 就被綁定成 43 , 而尾就綁定到通配符 _ 昨悼,被直接扔掉了蝗锥。

使用提取器

那到底該在什么時候使用、怎么使用自定義的提取器呢率触?尤其考慮到终议,使用樣例類就能自動獲得可用的提取器。

一些人指出葱蝗,使用樣例類穴张、對樣例類進行模式匹配打破了封裝, 耦合了匹配數據和其具體實現的方式两曼,這種批評通常是從面向對象的角度出發(fā)的皂甘。 如果想用 Scala 進行函數式編程,將樣例類當作只包含純數據(不包含行為)的 代數數據類型 悼凑,那它非常適合偿枕。

通常,只有當從無法掌控的類型中提取數據户辫,或者是需要其他進行模式匹配的方法時渐夸,才需要實現自己的提取器。

提取器的一種常見用法是從字符串中提取出有意義的值寸莫, 作為練習捺萌,想一想如何實現 URLExtractor以匹配代表 URL 的字符串。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市桃纯,隨后出現的幾起案子酷誓,更是在濱河造成了極大的恐慌,老刑警劉巖态坦,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盐数,死亡現場離奇詭異,居然都是意外死亡伞梯,警方通過查閱死者的電腦和手機玫氢,發(fā)現死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谜诫,“玉大人漾峡,你說我怎么就攤上這事∮骺酰” “怎么了生逸?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長且预。 經常有香客問我槽袄,道長,這世上最難降的妖魔是什么锋谐? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任遍尺,我火速辦了婚禮,結果婚禮上涮拗,老公的妹妹穿的比我還像新娘乾戏。我一直安慰自己,他們只是感情好多搀,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布歧蕉。 她就那樣靜靜地躺著,像睡著了一般康铭。 火紅的嫁衣襯著肌膚如雪惯退。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天从藤,我揣著相機與錄音催跪,去河邊找鬼。 笑死夷野,一個胖子當著我的面吹牛懊蒸,可吹牛的內容都是我干的。 我是一名探鬼主播悯搔,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼骑丸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起通危,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤铸豁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后菊碟,有當地人在樹林里發(fā)現了一具尸體节芥,經...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年逆害,在試婚紗的時候發(fā)現自己被綠了头镊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡魄幕,死狀恐怖相艇,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情梅垄,我是刑警寧澤厂捞,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站队丝,受9級特大地震影響,放射性物質發(fā)生泄漏欲鹏。R本人自食惡果不足惜机久,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赔嚎。 院中可真熱鬧膘盖,春花似錦、人聲如沸尤误。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽历恐。三九已至备典,卻和暖如春阁将,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喘落。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留最冰,地道東北人瘦棋。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像暖哨,于是被迫代替她去往敵國和親赌朋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內容