Scala implicit 隱式轉換安全駕駛指南

這篇短文將結合實例對隱式轉換的各種場景進行解釋和總結,希望看完的人能夠安全駛過隱式轉換這個大坑。

隱式轉換函數

隱式轉換函數有兩種作用場景。

  • 1 轉換為期望類型:就是指一旦編譯器看到X,但需要Y呻右,就會檢查從X到Y的隱式轉換函數。
  • 2 轉換方法的調用者:簡單來說鞋喇,如obj.f()声滥,如果obj對象沒有f方法,則嘗試將obj轉換為擁有f方法的類型侦香。
object ImpFunction extends App {

  class Dog(val name: String) {
    def bark(): Unit = println(s"$name say: Wang !")
  }

  implicit def double2int(d: Double): Int = d.toInt

  implicit def string2Dog(s: String): Dog = new Dog(s)

  val f: Int = 1.1 //轉換為期望類型,1.1通過double2int轉成了Int類型

  println(f)

  "Teddy".bark() // 轉換方法的調用者,字符串通過string2Dog轉成了Dog, 于是有了bark方法

}
// output
// 1
// Teddy say: Wang !

val f: Int = 1.1 因為類型不匹配落塑,這段本來是無法通過編譯的,但是編譯器發(fā)現存在一個Double至Int的隱式轉換函數罐韩,所以進行了隱式轉換憾赁。

"Teddy".bark() String類型本來是沒有bark方法的,但是編譯器發(fā)現了隱式轉換string2Dog可以使得String轉成一種擁有bark方法的類型散吵,相當于進行了這樣的轉換:string2Dog("Teddy").bark()龙考。

注意事項

需要注意的是,編譯器只關心隱式轉換函數的輸入輸出類型矾睦,不關心函數名晦款,為避免歧義,同一個作用域中不能有輸入輸出類型相同的兩個隱式轉換函數枚冗,不然編譯器會報錯缓溅。

隱式類

Scala 2.10引入了一種叫做隱式類的新特性。隱式類指的是用implicit關鍵字修飾的類赁温。使用情況與隱式轉換函數類似坛怪,可以看做將類的構造函數定義為隱式轉換函數,返回類型就是這個類股囊。

package io.github.liam8.impl

object ImpClass extends App {

  implicit class Dog(val name: String) {
    def bark(): Unit = println(s"$name say: Wang !")
  }

  "Teddy".bark()

}

注意事項

這段來自官網IMPLICIT CLASSES
隱式類有以下限制條件:

  • 1 只能在別的trait/類/對象內部定義袜匿。
    object Helpers {
       implicit class RichInt(x: Int) // 正確!
    }
    implicit class RichDouble(x: Double) // 錯誤毁涉!
  • 2 構造函數只能攜帶一個非隱式參數沉帮。
    implicit class RichDate(date: java.util.Date) // 正確锈死!
    implicit class Indexer[T](collecton: Seq[T], index: Int) // 錯誤贫堰!
    implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 正確穆壕!

雖然我們可以創(chuàng)建帶有多個非隱式參數的隱式類,但這些類無法用于隱式轉換其屏。

  • 3 在同一作用域內喇勋,不能有任何方法、成員或對象與隱式類同名偎行。
    注意:這意味著隱式類不能是case class川背。
    object Bar
    implicit class Bar(x: Int) // 錯誤!

    val x = 5
    implicit class x(y: Int) // 錯誤蛤袒!

    implicit case class Baz(x: Int) // 錯誤熄云!

隱式參數 & 隱式值

package io.github.liam8.impl

object ImpParam extends App {

  def bark(implicit name: String): Unit = println(s"$name say: Wang !")

  implicit val t: String = "Hot Dog"

  bark

}

參數加上implicit就成了隱式參數,需要與隱式值(變量定義加上implicit)搭配使用妙真,最后一行的bark缺少了一個String類型的參數缴允,編譯器找到了String類型的隱式值,便將其傳入珍德,相當于執(zhí)行了bark(t)练般。

implicit關鍵字會作用于函數列表中的的所有參數,如def test(implicit x:Int, y: Double)這樣定義函數锈候,x和y就都成了隱式函數薄料。但是通常我們只希望部分參數為隱式參數,就好比通常會給部分參數提供默認值而不是全部都指定默認值泵琳,于是隱式參數常常與柯里化函數一起使用摄职,這樣可以使得只有最后一個參數為隱式參數,例如def test(x: Int)(implicit y: Double)虑稼。

??是完整的例子琳钉。

object ImpParamWithCurry extends App {

  def bark(name: String)(implicit word: String): Unit = println(s"$name say: $word !")

  implicit val w: String = "Wang"

  bark("Hot Dog")

}

注意事項

下面這段來自scala的隱式轉換學習總結(詳細)

  • 1)當函數沒有柯里化時,implicit關鍵字會作用于函數列表中的的所有參數蛛倦。
  • 2)隱式參數使用時要么全部不指定歌懒,要么全不指定,不能只指定部分溯壶。
  • 3)同類型的隱式值只能在作用域內出現一次及皂,即不能在同一個作用域中定義多個相同類型的隱式值。
  • 4)在指定隱式參數時且改,implicit 關鍵字只能出現在參數開頭验烧。
  • 5)如果想要實現參數的部分隱式參數,只能使用函數的柯里化又跛,
    如要實現這種形式的函數碍拆,def test(x:Int, implicit y: Double)的形式,必須使用柯里化實現:def test(x: Int)(implicit y: Double).
  • 6)柯里化的函數, implicit 關鍵字只能作用于最后一個參數感混。否則端幼,不合法。
  • 7)implicit 關鍵字在隱式參數中只能出現一次弧满,柯里化的函數也不例外婆跑!

隱式對象

類似于隱式值, 要結合隱式參數使用。先看一個栗子(下面的代碼需要認真體會)庭呜。

package io.github.liam8.impl

object ImpObject extends App {

  //定義一個`排序器`接口滑进,能夠比較兩個相同類型的值的大小
  trait Ordering[T] {
    //如果x<y返回-1,x>y返回1募谎,x==y則返回0.
    def compare(x: T, y: T): Int
  }

  //實現一個Int類型的排序器
  implicit object IntOrdering extends Ordering[Int] {
    override def compare(x: Int, y: Int): Int = {
      if (x < y) -1
      else if (x == y) 0
      else 1
    }
  }

  //實現一個String類型的排序器
  implicit object StringOrdering extends Ordering[String] {
    override def compare(x: String, y: String): Int = x.compareTo(y)
  }

  //一個通用的max函數
  def max[T](x: T, y: T)(implicit ord: Ordering[T]): T = {
      if (ord.compare(x, y) >= 0) x else y
  }

  println(max(1, 2))
  println(max("a", "b"))
}

//output: 
// 2
// b

max函數的作用顯然是返回x和y中的最大值扶关,但是x和y的值類型不是固定的,max不知道如何比較x和y的大型数冬,于是定義了一個隱式參數implicit ord: Ordering[T]驮审,希望能傳入一個Ordering[T]類型的排序器幫助進行x和y的比較。

在調用max(1, 2)的時候吉执,編譯器發(fā)現需要一個Ordering[Int]類型的參數疯淫,剛好implicit object IntOrdering定義了一個隱式對象符合要求,于是被用來傳入max函數戳玫。

隱式對象跟上面的隱式值非常相似熙掺,只是類型特殊而已。

在Scala中scala.math.Ordering很常用的內置特質咕宿,如果你理解了這段代碼币绩,也就大致理解了Ordering的原理。

上下文界定(context bounds)

這是一種隱式參數的語法糖府阀。

再看上面隱式對象的例子缆镣,如果要添加一個min函數,大致就是這樣

  def min[T](x: T, y: T)(implicit ord: Ordering[T]): T = {
    if (ord.compare(x, y) >= 0) y else x
  }

但是max和min函數的參數都比較長试浙,于是出現了一種簡化的寫法

  def min[T: Ordering](x: T, y: T): T = {
    val ord = implicitly[Ordering[T]]
    if (ord.compare(x, y) >= 0) y else x
  }

[T: Ordering]這種語法就叫上下文界定董瞻,含義是上下文中必須有一個Ordering[T]類型的隱式值,這個值會被傳入min函數田巴。但是由于這個隱式值并沒有明確賦值給某個變量钠糊,沒法直接使用它,所以需要一個implicitly函數把隱式值取出來壹哺。

implicitly函數的定義非常簡單抄伍,作用就是將T類型的隱含值返回:

@inline def implicitly[T](implicit e: T) = e

視界

這個語法已經被廢棄了,但是你還是可能會看到管宵,簡單解釋下截珍。

def min[T <% Ordered[T]](x: T, y: T): T = {
    if (x > y) y else x
}

視界的定義T <% Ordered[T]的含義是T可以被隱式轉換成Ordered[T]攀甚,這也是為什么x > y可以編譯通過。

上面的寫法其實等同于下面這樣岗喉,所以視界的語法不能用了也不要緊云稚。

  def min[T](x: T, y: T)(implicit c: T => Ordered[T]): T = {
    if (x > y) y else x
  }

隱式轉換機制

隱式轉換通用規(guī)則

  • 標記規(guī)則:只有標記為implicit的定義才是可用的。

  • 作用域規(guī)則:插入的隱式轉換必須以單一標識符的形式處于作用域中沈堡,或與轉換的源或目標類型關聯(lián)在一起。

單一標識符意思是不能插入形式為someVariable.convert(x)的轉換燕雁,只能是convert(x)诞丽。
單一標識符規(guī)則有個例外,編譯器還將在源類型或轉換的期望目標類型的伴生對象中尋找隱式定義拐格。

有點難理解?看個例子!

package io.github.liam8.impl

object ImpCompObject extends App {

  object Dog {
    implicit def dogToCat(d: Dog) = new Cat(d.name)
  }

  class Cat(val name: String) {
    def miao(): Unit = println(s"$name say: Miao !")
  }

  class Dog(val name: String) {
    def bark(): Unit = println(s"$name say: Wang !")
  }

  new Dog("Teddy").miao()

}
//Teddy say: Miao !

當前作用域中沒有定義和引入隱式函數僧免,但是在Dog的伴生對象中找到了,所以Dog可以被轉成Cat捏浊,這個跟上下文沒有關系懂衩,而是Dog自帶技能。

  • 無歧義規(guī)則:隱式轉換唯有不存在其他轉換的前提下有效金踪。

  • 單一調用規(guī)則:只會嘗試一個隱式操作浊洞。

  • 顯示操作先行規(guī)則:若編寫的代碼類型檢查無誤,則不會嘗試隱式操作胡岔。

轉換時機

  • 當類型與目標類型不一致時
  • 當對象調用類中不存在的方法或成員時
  • 缺少隱式參數時

也即是能用到隱式操作的有三個地方:轉換為期望類型法希、指定(方法)調用者的轉換、隱式參數靶瘸。

轉換機制

這段來自深入理解Scala的隱式轉換

即編譯器是如何查找到缺失信息的苫亦,解析具有以下兩種規(guī)則:

  • 1.首先會在當前代碼作用域下查找隱式實體(隱式方法 隱式類 隱式對象)

  • 2.如果第一條規(guī)則查找隱式實體失敗,會繼續(xù)在隱式參數的類型的作用域里查找
    類型的作用域是指與該類型相關聯(lián)的全部伴生模塊怨咪,一個隱式實體的類型T它的查找范圍如下:

    • 1 如果T被定義為T with A with B with C,那么A,B,C都是T的部分屋剑,在T的隱式解析過程中,它們的伴生對象都會被搜索
    • 2 如果T是參數化類型诗眨,那么類型參數和與類型參數相關聯(lián)的部分都算作T的部分唉匾,比如List[String]的隱式搜索會搜索List的伴生對象和String的伴生對象
    • 3 如果T是一個單例類型p.T,即T是屬于某個p對象內匠楚,那么這個p對象也會被搜索
    • 4 如果T是個類型注入S#T肄鸽,那么S和T都會被搜索

上路前的話

這段話來自《Scala編程》

隱式操作若過于頻繁使用,會讓代碼變得晦澀難懂油啤。因此典徘,在考慮添加新的隱式轉換之前,請首先自問是否能夠通過其他手段益咬,諸如繼承逮诲、混入組合或方法重載帜平,達到同樣的目的。如果所有這些都不能成功梅鹦,并且你感覺代碼仍有一些繁復和冗余裆甩,那么隱式操作或許正好能幫到你。

所以齐唆。嗤栓。。謹慎使用箍邮,小心翻車茉帅,good luck!

參考文獻

IMPLICIT CLASSES

scala的隱式轉換學習總結(詳細)

《Scala編程》

深入理解Scala的隱式轉換

本文代碼

Github倉庫

轉載請注明原文地址:https://liam-blog.ml/2019/09/28/scala-implicit/

查看更多博主文章

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锭弊,隨后出現的幾起案子堪澎,更是在濱河造成了極大的恐慌,老刑警劉巖味滞,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件樱蛤,死亡現場離奇詭異,居然都是意外死亡剑鞍,警方通過查閱死者的電腦和手機昨凡,發(fā)現死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蚁署,“玉大人土匀,你說我怎么就攤上這事⌒斡茫” “怎么了就轧?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長田度。 經常有香客問我妒御,道長,這世上最難降的妖魔是什么镇饺? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任乎莉,我火速辦了婚禮,結果婚禮上奸笤,老公的妹妹穿的比我還像新娘惋啃。我一直安慰自己,他們只是感情好监右,可當我...
    茶點故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布边灭。 她就那樣靜靜地躺著,像睡著了一般健盒。 火紅的嫁衣襯著肌膚如雪绒瘦。 梳的紋絲不亂的頭發(fā)上称簿,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天,我揣著相機與錄音惰帽,去河邊找鬼憨降。 笑死,一個胖子當著我的面吹牛该酗,可吹牛的內容都是我干的授药。 我是一名探鬼主播,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼呜魄,長吁一口氣:“原來是場噩夢啊……” “哼悔叽!你這毒婦竟也來了?” 一聲冷哼從身側響起耕赘,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎膳殷,沒想到半個月后操骡,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡赚窃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年册招,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勒极。...
    茶點故事閱讀 38,687評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡是掰,死狀恐怖,靈堂內的尸體忽然破棺而出辱匿,到底是詐尸還是另有隱情键痛,我是刑警寧澤,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布匾七,位于F島的核電站絮短,受9級特大地震影響,放射性物質發(fā)生泄漏昨忆。R本人自食惡果不足惜丁频,卻給世界環(huán)境...
    茶點故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望邑贴。 院中可真熱鬧席里,春花似錦、人聲如沸拢驾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽繁疤。三九已至署穗,卻和暖如春寥裂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背案疲。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工封恰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人褐啡。 一個月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓诺舔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親备畦。 傳聞我的和親對象是個殘疾皇子低飒,可洞房花燭夜當晚...
    茶點故事閱讀 43,576評論 2 349

推薦閱讀更多精彩內容