【Scala】Scala 隱式轉(zhuǎn)換 implicit

本篇結(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 classcase 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

四、參考博文

1.Scala中的Implicit詳解

2.Scala 隱式轉(zhuǎn)換implicit詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容