Key Words: Implicit conversions
, Implicit parameters
, Implicit class
, Implicitly
, Context bound
隱式轉(zhuǎn)換(Implicit conversions)
隱式定義就是允許編譯器插入一段程序來解決一些類型錯(cuò)誤(type errors)的手段
對(duì)于兩個(gè)軟件體亡脑,彼此設(shè)計(jì)時(shí)都沒有考慮到對(duì)方,此時(shí)隱式轉(zhuǎn)換通常比較有用邀跃。隱式轉(zhuǎn)換可以減少代碼中將一個(gè)類型轉(zhuǎn)換為另一個(gè)類型的顯示轉(zhuǎn)換(explicit conversions)
隱式規(guī)則
標(biāo)記規(guī)則(Marking rule)
只有標(biāo)記為
implicit
的定義才可用霉咨。可以用 implicit 來標(biāo)記任何變量拍屑、函數(shù)或?qū)ο蠖x
編譯器只會(huì)在顯示標(biāo)識(shí)為 implicit 的定義中進(jìn)行選擇
作用域規(guī)則(Scope rule)
被插入的隱式轉(zhuǎn)換必須是當(dāng)前作用域的單個(gè)標(biāo)識(shí)符(single identifier)途戒,或者跟隱式轉(zhuǎn)換的源類型或目標(biāo)類型有關(guān)聯(lián)
Scala 編譯器只會(huì)考慮那些在作用域內(nèi)的隱式轉(zhuǎn)換。因此僵驰,必須以某種方式將隱式轉(zhuǎn)換定義引入到當(dāng)前作用域才能使得它們可用
編譯器還會(huì)在隱式轉(zhuǎn)換的源類型或目標(biāo)類型的伴生對(duì)象中查找隱式定義
對(duì)于類庫而言喷斋,常見的做法是提供一個(gè)包含了一些有用的隱式轉(zhuǎn)換的 Preamble 對(duì)象,這樣使用這個(gè)類庫的代碼就可以通過一個(gè)“import Preamble._”來訪問該類庫的隱式轉(zhuǎn)換
作用域規(guī)則有助于模塊化推理(modular reasoning)矢渊。當(dāng)你閱讀一個(gè)代碼文件继准,你只需要考慮被引用的或者是使用全限定名的定義。這樣顯示寫代碼的好處對(duì)于對(duì)于隱式(implicits)也同樣的重要矮男。如果隱式在系統(tǒng)范圍(system-wide)內(nèi)有效移必,那么想理解一個(gè)源文件就不得不了解每個(gè)被引入的隱式
每次一個(gè)規(guī)則(One-at-a-time rule)
每次只能有一個(gè)隱式定義被插入
這個(gè)規(guī)則能夠減少有問題代碼的編譯時(shí)間,以及減少程序員編寫的和程序?qū)嶋H做的之間的差異
可以通過在隱式定義中使用隱式參數(shù)來繞過這個(gè)規(guī)則
// 下面的代碼只是為了說明繞過每次一個(gè)規(guī)則(One-at-a-time rule)毡鉴,沒有任何實(shí)際意義
implicit vall height = 1
case class Foo(width: Int, height: Int) {
def bar = println(s"Foo: ${width} ${height}")
}
implicit def convert2Foo(width: Int)(implicit height: Int) = Foo(width, height)
1 bar
// 這樣就能讓編譯器插入兩個(gè)隱式定義了
顯示優(yōu)先原則(Explicits-first rule)
只要代碼按編寫的樣子能夠通過類型檢查崔泵,編譯器就不會(huì)嘗試隱式定義
其實(shí),到底留多少隱式轉(zhuǎn)換給編譯器來插入猪瞬,最終是代碼風(fēng)格的問題
隱式轉(zhuǎn)換命名
隱式轉(zhuǎn)換可以使用任意的名字憎瘸。只有在兩個(gè)地方對(duì)于命名需要進(jìn)行注意:
1.在方法的應(yīng)用中,顯示的寫出隱式轉(zhuǎn)換
2.決定哪些隱式可以在程序的哪些地方可用
隱式應(yīng)用場(chǎng)景
在 Scala 中隱式有三個(gè)應(yīng)用場(chǎng)景:類型的轉(zhuǎn)換(conversions to an expected type)陈瘦、接受者的轉(zhuǎn)換(conversions fo the receiver of a selection)以及隱式參數(shù)(implicit parameters)
類型的轉(zhuǎn)換
就是在上下文中使用一種類型幌甘,實(shí)際上期待另外一種不同的類型
不論何時(shí),編譯器看見一個(gè)類型 X痊项,但是實(shí)際需要的是 Y 類型锅风,編譯器就會(huì)尋找隱式函數(shù)來將 X 轉(zhuǎn)換為 Y
implicit def double2Int(d: Double) = d.toInt
val i: Int = 3.5
此處的類型轉(zhuǎn)換有一個(gè)原則:將更多約束的類型轉(zhuǎn)換為一個(gè)更為普通的類型
implicit def int2Double(i: Int) = i.toDouble
val d: Double = 1
接受者的轉(zhuǎn)換
可以通俗的理解,在一個(gè)對(duì)象上調(diào)用該對(duì)象并不存在的某個(gè)方法鞍泉,編譯器會(huì)幫你挑選合適的隱式轉(zhuǎn)換皱埠,來將此對(duì)象轉(zhuǎn)換為含有該方法的對(duì)象
case class Foo(f: Int)
case class Bar(b: Int) {
def doSome = println(s"hello bar: $!")
}
implicit def foo2Bar(foo: Foo) = Bar(foo.f)
Foo(1) doSome
接受者的轉(zhuǎn)換主要有兩個(gè)用處:第一咖驮,可以平滑的集成一個(gè)新類到一個(gè)已有的類層次中边器;第二训枢,支持編寫 DSL(domain-specific language)
隱式類(Implicit class)
編譯器通過隱式類的類參數(shù)生成一個(gè)隱式轉(zhuǎn)換,該隱式將類參數(shù)轉(zhuǎn)換為該類本身
implicit class WaveArrow[A](x: A) {
def ~> [B](y: B) = new Tuple2(x, y)
}
隱式類的定義有如下約束:
1.隱式類不能是一個(gè)樣例類(case class)忘巧,且構(gòu)造器只能含有一個(gè)形參
2.隱式類必須位于某個(gè)對(duì)象恒界、類或者特質(zhì)當(dāng)中
在實(shí)際應(yīng)用當(dāng)中,如果使用隱式類作為富包裝來給已有的類來添加一些方法砚嘴,上述的約束就無關(guān)緊要了
// 上面的隱式類定義仗处,編譯器會(huì)做如下等效的工作
implicit def WaveArrow[A](x: A) = new WaveArrow[A](x)
Map(1 ~> 2)
隱式參數(shù)
隱式參數(shù)給調(diào)用者提供更多被調(diào)用的函數(shù)的信息,通常用在泛型函數(shù)中
隱式參數(shù)用于最后一個(gè)
被柯里化
后的參數(shù)列表枣宫,而不是最后一個(gè)參數(shù)
由于隱式參數(shù)的匹配是通過參數(shù)的類型來進(jìn)行的婆誓,所以要求隱式參數(shù)的類型盡可能的罕見與特別("rare" or "special"),以此來防止匹配的失誤
隱式參數(shù)還經(jīng)常被用來提供關(guān)于之前參數(shù)列表中顯示提到的類型的信息(Implicit parameters is that they are perhaps most often used to provide information about a type mentioned explicitly in an earlier parameter list)
通過下面例子來理解上述語句也颤,隱式參數(shù) ordering 的類型是 Ordering[T]洋幻,在此例中,該參數(shù)提供了更多關(guān)于類型 T 的信息翅娶,也就是如何對(duì) T 進(jìn)行排序文留。類型 T 在 List[T] 中被提及,也就是列表中元素的類型竭沫,而 List[T] 出現(xiàn)在 Ordering[T] 之前
def max[T](xs: List[T])(implicit ordering: Ordering[T]): T = {
xs match {
case List() => throw IllegalArgumentException
case List(x) => x
case x :: last => {
val m = max(last)(ordering)
if (ordering.gt(x, m)) x else m
}
}
}
隱式參數(shù)的風(fēng)格規(guī)則
1.使用定制化的命名類型燥翅,也就是說不要使用通用類型
// 使用定制化的命名類型
class Drink(s: String)
implicit val drink: Drink = new Drink("maotai")
def drinkSome(implicit drink: Drink) = {}
// 不要使用通用的類型
implicit val drink: String = "maotai"
def drinkSome(implicit drink: String) = {}
2.在隱式參數(shù)類型中至少使用一個(gè)能夠定義角色的名稱
def maxListPoorStyle[T](elems: List[T])(implicit orderer: (T, T) => Boolean): T
上述隱式參數(shù)的表達(dá)方式就會(huì)給讀者帶來困擾,也不能提供關(guān)于類型 T 的更多信息(比如函數(shù)式判斷相等蜕提、大于還是小于)
上下文邊界(Context bounds)
當(dāng)在一個(gè)函數(shù)的形參上面使用隱式森书,編譯器不僅試著用隱式值來補(bǔ)充該形參,而且在方法體中把該形參當(dāng)做可用的隱式
def max[T](xs: List[T])(implicit ordering: Ordering[T]): T = {
xs match {
case List() => throw IllegalArgumentException
case List(x) => x
case x :: last => {
val m = max(last) // (ordering) is implicit
if (ordering.gt(x, m)) x else m // this ordering is still explicit
}
}
}
有種方式能夠減少代碼中對(duì)于 ordering 的使用
def implicitly[T](implicit t: T) = t // 存在于標(biāo)準(zhǔn)庫中
def max[T](xs: List[T])(implicit ordering: Ordering[T]): T = {
xs match {
case List() => throw IllegalArgumentException
case List(x) => x
case x :: last => {
val m = max(last) // (ordering) is implicit
if (implicitly[Ordering[T]].gt(x, m)) x else m // this ordering is implicit now
}
}
}
// 這時(shí)你可以不用關(guān)注隱式參數(shù)具體的名字了
def max[T](xs: List[T])(implicit comparator: Ordering[T]): T = {
xs match {
case List() => throw IllegalArgumentException
case List(x) => x
case x :: last => {
val m = max(last) // (ordering) is implicit
if (implicitly[Ordering[T]].gt(x, m)) x else m // this ordering is implicit now
}
}
}
通過如上的表述谎势,隱式參數(shù)的形參名字并不重要凛膏。Scala 可以使用上下文邊界
來省略隱式參數(shù)的定義精簡(jiǎn)方法頭
def max[T : Ordering](xs: List[T]): T = {
xs match {
case List() => throw IllegalArgumentException
case List(x) => x
case x :: last => {
val m = max(last) // (ordering) is implicit
if (implicitly[Ordering[T]].gt(x, m)) x else m // this ordering is implicit now
}
}
}
上下文邊界,可以把它理解為隱式參數(shù)使用的一個(gè)語法糖
隱式轉(zhuǎn)換適用情況
多轉(zhuǎn)換同時(shí)適用
在同一個(gè)作用域下脏榆,有多個(gè)轉(zhuǎn)換同時(shí)能夠工作猖毫,編譯器為了不制造困惑,會(huì)拒絕插入轉(zhuǎn)換
但是在 Scala 2.8 時(shí)放松了這個(gè)規(guī)則须喂。如果一個(gè)可用的轉(zhuǎn)換比另外一個(gè)轉(zhuǎn)換更加嚴(yán)格且具體吁断,編譯器會(huì)選擇更加具體的轉(zhuǎn)換
怎樣才能認(rèn)為更加具體呢?
1.前者參數(shù)的類型是后者的子類
2.如果轉(zhuǎn)換都是方法坞生,前者封裝的類擴(kuò)展子后者封裝的類
調(diào)試
scalac Xprint:typer
xxx.scala