這篇短文將結合實例對隱式轉換的各種場景進行解釋和總結,希望看完的人能夠安全駛過隱式轉換這個大坑。
隱式轉換函數
隱式轉換函數有兩種作用場景。
- 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!
參考文獻
《Scala編程》