本篇結(jié)構(gòu):
- 前言
- 隱式轉(zhuǎn)換類(lèi)型
- 隱式轉(zhuǎn)換的規(guī)則 -- 如何尋找隱式轉(zhuǎn)換方法
- 參考博文
一、Implicit 簡(jiǎn)介
implicit,即隱式轉(zhuǎn)換,是支撐 Scala 易用、容錯(cuò)以及靈活語(yǔ)法的基礎(chǔ)。
Scala 的隱式轉(zhuǎn)換系統(tǒng)定義了一套良好的查找機(jī)制,當(dāng)代碼出現(xiàn)類(lèi)型編譯錯(cuò)誤時(shí),編譯器試圖去尋找一個(gè)隱式 implicit 的轉(zhuǎn)換方法,轉(zhuǎn)換出正確的類(lèi)型,從而使得編譯器能夠自我修復(fù),完成編譯。
在 Scala 語(yǔ)言當(dāng)中,隱式轉(zhuǎn)換是一項(xiàng)強(qiáng)大的程序語(yǔ)言功能,它不僅能夠簡(jiǎn)化程序設(shè)計(jì),也能夠使程序具有很強(qiáng)的靈活性,可以在不修改原有的類(lèi)的基礎(chǔ)上,對(duì)類(lèi)的功能進(jìn)行擴(kuò)展,這對(duì)一些舊系統(tǒng)的升級(jí)十分便利。
通過(guò)隱式轉(zhuǎn)換,可以在編寫(xiě) Scala 程序時(shí)故意漏掉一些信息,讓編譯器去嘗試在編譯期間自動(dòng)推導(dǎo)出這些信息來(lái),這種特性可以極大的減少代碼量,忽略那些冗長(zhǎng),過(guò)于細(xì)節(jié)的代碼。
比如,在 Spark 源碼中,經(jīng)常會(huì)發(fā)現(xiàn) RDD 這個(gè)類(lèi)沒(méi)有 reduceByKey、groupByKey 等方法定義,但是卻可以在 RDD 上調(diào)用這些方法。這就是 Scala 隱式轉(zhuǎn)換導(dǎo)致的。
二、隱式轉(zhuǎn)換類(lèi)型
Scala 隱式轉(zhuǎn)換類(lèi)型主要包括以下幾種類(lèi)型:隱式參數(shù)、隱式試圖、隱式類(lèi)。
2.1、隱式參數(shù)
隱式參數(shù)是在編譯器找不到函數(shù)需要某種類(lèi)型的參數(shù)時(shí)的一種修復(fù)機(jī)制。
object ImplicitParam {
def foo(amount: Float)(implicit rate: Float): Unit = {
println(amount * rate)
}
def main(args: Array[String]): Unit = {
// 隱式參數(shù)
implicit val r: Float = 0.13F // 定義隱式變量
foo(10) // 輸出1.3
}
}
在函數(shù)定義的時(shí)候,支持在最后一組參數(shù)使用 implicit,表明這是一組隱式參數(shù)。在調(diào)用該函數(shù)的時(shí)候,可以不用傳遞隱式參數(shù),而編譯器會(huì)自動(dòng)尋找一個(gè) implict 標(biāo)記過(guò)的合適的值作為該參數(shù)。
trait Adder[T] {
def add(x: T, y: T): T
}
implicit val a: Adder[Int] = new Adder[Int] {
override def add(x: Int, y: Int): Int = {
println(x + y)
x + y
}
}
def addTest(x: Int, y: Int)(implicit adder: Adder[Int]): Int = {
adder.add(x, y)
}
addTest(1, 2) // 正確, = 3
addTest(1, 2)(a) // 正確, = 3
addTest(1, 2)(new Adder[Int] {
override def add(x: Int, y: Int): Int = {
println(x - y)
x - y
}
}) // 同樣正確, = -1
2.2、隱式視圖
隱式視圖,是指把一種類(lèi)型自動(dòng)轉(zhuǎn)換到另外一種類(lèi)型,以符合表達(dá)式的要求。
隱式視圖定義一般用如下形式:implicit def <ConversionName> (<argumentName>: OriginalType): ViewType。在需要的時(shí)候,如果隱式作用域里存在這個(gè)定義,它會(huì)隱式地把 OriginalType 類(lèi)型的值轉(zhuǎn)換為ViewType 類(lèi)型的值。
隱式視圖包含兩種轉(zhuǎn)換類(lèi)型:隱式類(lèi)型轉(zhuǎn)換以及隱式方法調(diào)用。
2.2.1、隱式類(lèi)型轉(zhuǎn)換
隱式類(lèi)型轉(zhuǎn)換是編譯器發(fā)現(xiàn)傳遞的數(shù)據(jù)類(lèi)型與申明不一致時(shí),編譯器在當(dāng)前作用域查找類(lèi)型轉(zhuǎn)換方法,對(duì)數(shù)據(jù)類(lèi)型進(jìn)行轉(zhuǎn)換。
舉個(gè)例子:
object ImplicitView {
def main(args: Array[String]): Unit = {
// 隱式類(lèi)型轉(zhuǎn)換
implicit def double2Int(d: Double): Int = d.toInt
val i: Int = 3.5
println(i)
}
}
變量i申明為Int類(lèi)型,但是賦值Double類(lèi)型數(shù)據(jù),顯然編譯通不過(guò)。這個(gè)時(shí)候可以借助隱式類(lèi)型轉(zhuǎn)換,定義Double轉(zhuǎn)Int規(guī)則,編譯器就會(huì)自動(dòng)查找該隱式轉(zhuǎn)換,將3.5轉(zhuǎn)換成3,從而達(dá)到編譯器自動(dòng)修復(fù)效果。
2.2.2、隱式方法調(diào)用
隱式方法調(diào)用是當(dāng)編譯器發(fā)現(xiàn)一個(gè)對(duì)象存在未定義的方法調(diào)用時(shí),就會(huì)在當(dāng)前作用域中查找是否存在對(duì)該對(duì)象的類(lèi)型隱式轉(zhuǎn)換,如果有,就查找轉(zhuǎn)換后的對(duì)象是否存在該方法,存在,則調(diào)用。
object ImplicitView2 {
class Horse {
def drinking(): Unit = {
println("I can drinking")
}
}
class Crow {}
object drinking {
// 隱式方法調(diào)用
implicit def extendSkill(c: Crow): Horse = new Horse()
}
def main(args: Array[String]): Unit = {
// 隱式轉(zhuǎn)換調(diào)用類(lèi)中不存在的方法
import drinking._
val crow = new Crow()
crow.drinking()
}
}
crow對(duì)象并沒(méi)有drinkging()方法定義,但是通過(guò)隱式規(guī)則轉(zhuǎn)換,可以擴(kuò)展crow對(duì)象功能,使其可以擁有Horse對(duì)象的功能。
這也是隱式轉(zhuǎn)換最常用的用途:擴(kuò)展已有的類(lèi),在不修改原有類(lèi)的基礎(chǔ)上為其添加新的方法、成員。
2.2.3、隱式視圖使用注意
- 不接受多參數(shù)
對(duì)于隱式視圖,編譯器最關(guān)心的是它的類(lèi)型簽名,即它將哪一種類(lèi)型轉(zhuǎn)換到另一種類(lèi)型,也就是說(shuō)它應(yīng)該接受只一個(gè)參數(shù),對(duì)于接受多參數(shù)的隱式函數(shù)來(lái)說(shuō)就沒(méi)有隱式轉(zhuǎn)換的功能了。
implicit def int2str(x:Int):String = x.toString // 正確
implicit def int2str(x:Int,y:Int):String = x.toString // 錯(cuò)誤
- 不支持嵌套的隱式轉(zhuǎn)換
class A{
def hi: Unit = println("hi")
}
implicit def int2str(x:Int):String = x.toString
implicit def str2A(x:String):A = new A
"str".hi // 正確
1.hi // 錯(cuò)誤
- 不能存在二義性,即同一個(gè)作用域不能定義兩個(gè)相同類(lèi)型的隱式轉(zhuǎn)換函數(shù),這樣編譯器將無(wú)法決定使用哪個(gè)轉(zhuǎn)換
/* 錯(cuò)誤-- */
implicit def int2str(x:Int):String = x.toString
implicit def anotherInt2str(x:Int):A = x.toString
/* --錯(cuò)誤 */
2.3、隱式類(lèi)
Scala 2.10引入了一種叫做隱式類(lèi)的新特性。隱式類(lèi)指的是用implicit關(guān)鍵字修飾的類(lèi)。在對(duì)應(yīng)的作用域內(nèi),帶有這個(gè)關(guān)鍵字的類(lèi)的主構(gòu)造函數(shù)可用于隱式轉(zhuǎn)換。
object ImplicitClass {
class Crow {}
object crow_eval {
implicit class Parrot(animal: Crow) {
def say(say: String): Unit = {
println(s"I have the skill of Parrot: $say")
}
}
}
def main(args: Array[String]): Unit = {
// 隱式類(lèi)
import crow_eval._
val crow: Crow = new Crow
crow.say("balabala")
}
}
2.3.1、隱式類(lèi)使用注意
- 只能在別的 trait/類(lèi)/對(duì)象內(nèi)部定義,即隱式類(lèi)必須被定義在類(lèi)、伴生對(duì)象和包對(duì)象里
object Helpers {
implicit class RichInt(x: Int) // 正確!
}
implicit class RichDouble(x: Double) // 錯(cuò)誤!
- 構(gòu)造參數(shù)有且只有一個(gè),且為非隱式參數(shù)
implicit class RichDate(date: java.util.Date) // 正確!
implicit class Indexer[T](collecton: Seq[T], index: Int) // 錯(cuò)誤!
implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 錯(cuò)誤!
- 隱式類(lèi)不能是
case class(case class會(huì)自動(dòng)生成伴生對(duì)象,與上一條矛盾)
implicit case class Baz(x: Int) // 錯(cuò)誤!
- 作用域內(nèi)不能有與之同名的標(biāo)識(shí)符
三、隱式轉(zhuǎn)換的規(guī)則 -- 如何尋找隱式轉(zhuǎn)換方法
Scala 編譯器是按照怎樣的套路來(lái)尋找一個(gè)可以應(yīng)用的隱式轉(zhuǎn)換方法呢? 在Martin Odersky 的 Programming in Scala, First Edition 中總結(jié)了以下幾條原則:
object StringOpsTest extends App {
// 定義打印操作Trait
trait PrintOps {
val value: String
def printWithSeperator(sep: String): Unit = {
println(value.split("").mkString(sep))
}
}
// 定義針對(duì)String的隱式轉(zhuǎn)換方法
implicit def stringToPrintOps(str: String): PrintOps = new PrintOps {
override val value: String = str
}
// 定義針對(duì)Int的隱式轉(zhuǎn)換方法
implicit def intToPrintOps(i: Int): PrintOps = new PrintOps {
override val value: String = i.toString
}
// String 和 Int 都擁有 printWithSeperator 函數(shù)
"hello,world" printWithSeperator "*"
1234 printWithSeperator "*"
}
3.1、標(biāo)記規(guī)則
只會(huì)去尋找?guī)в?code>implicit標(biāo)記的方法,這點(diǎn)很好理解,在上面的代碼也有演示,如果不申明為implicit,只能手工去調(diào)用。
3.2、作用域范圍規(guī)則
只會(huì)在當(dāng)前表達(dá)式的作用范圍之內(nèi)查找,而且只會(huì)查找單一標(biāo)識(shí)符的函數(shù),上述代碼中,如果stringToPrintOps方法封裝在其他對(duì)象(比如叫Test)中,雖然Test對(duì)象也在作用域范圍之內(nèi),但編譯器不會(huì)嘗試使用Test.stringToPrintOps進(jìn)行轉(zhuǎn)換,這就是單一標(biāo)識(shí)符的概念。
單一標(biāo)識(shí)符有一個(gè)例外,如果stringToPrintOps方法在PrintOps的伴生對(duì)象中申明也是有效的,Scala 編譯器也會(huì)在源類(lèi)型或目標(biāo)類(lèi)型的伴生對(duì)象內(nèi)查找隱式轉(zhuǎn)換方法,本規(guī)則只會(huì)在轉(zhuǎn)型有效。而一般的慣例,會(huì)將隱式轉(zhuǎn)換方法封裝在伴生對(duì)象中。
當(dāng)前作用域上下文的隱式轉(zhuǎn)換方法優(yōu)先級(jí)高于伴生對(duì)象內(nèi)的隱式方法。
3.3、不能有歧義原則
在相同優(yōu)先級(jí)的位置只能有一個(gè)隱式的轉(zhuǎn)型方法,否則Scala編譯器無(wú)法選擇適當(dāng)?shù)倪M(jìn)行轉(zhuǎn)型,編譯出錯(cuò)。
3.4、只應(yīng)用轉(zhuǎn)型方法一次原則
Scala編譯器不會(huì)進(jìn)行多次隱式方法的調(diào)用,比如需要C類(lèi)型參數(shù),而實(shí)際類(lèi)型為A,作用域內(nèi)存在A => B,B => C的隱式方法,Scala編譯器不會(huì)嘗試先調(diào)用A => B ,再調(diào)用B => C。
3.5、顯示方法優(yōu)先原則
如果方法被重載,可以接受多種類(lèi)型,而作用域中存在轉(zhuǎn)型為另一個(gè)可接受的參數(shù)類(lèi)型的隱式方法,則不會(huì)被調(diào)用,Scala編譯器優(yōu)先選擇無(wú)需轉(zhuǎn)型的顯式方法。
def m(a: A): Unit = ???
def m(b: B): Unit = ???
val b: B = new B
//存在一個(gè)隱式的轉(zhuǎn)換方法 B => A
implicit def b2a(b: B): A = ???
m(b) //隱式方法不會(huì)被調(diào)用,優(yōu)先使用顯式的 m(b: B): Unit