1. 程式人生 > >扒一扒scala隱式轉換

扒一扒scala隱式轉換

操作規則:

1.作用域,scala編譯器將僅考慮作用域之內的隱式轉換,要使用某種隱式操作,你必須以單一識別符號的形式(一種情況例外)將其帶入作用域之內。例如:

object TestImplicit {
  implicit def doubleToInt(x: Double) = x.toInt
}
object test {
  def main(args: Array[String]): Unit = {
    import layne.impilcit.TestImplicit._//以單一識別符號引進doubleToInt的隱式轉換
    val i: Int = 2.3
  }
}

單一識別符號有一個外例,編譯器還將在源型別和目標型別的伴生物件中尋找隱式定義。

2.無歧義,比方說 x+y需要用到隱式轉換,但是在作用域內卻找到不止一個隱式定義與之匹配,將不能編譯通過。

object TestImplicit {
  implicit def doubleToInt(x: Double) = x.toInt
}
object TestImplicit2{
  implicit def doubleToInt(x: Double) = x.toInt
}
object test {
  def main(args: Array[String]): Unit = {
    import
layne.impilcit.TestImplicit._ import layne.impilcit.TestImplicit2._ val i: Int = 2.3//能匹配多個隱式定義,編譯將報錯 } }

用在何處:

1.轉換為期望型別:
在互動模式下直接定義

scala> val i: Int = 3.5
<console>:7: error: type mismatch;
 found : Double(3.5)
 required: Int
  val i: Int = 3.5
  ^

將報型別錯誤
我們定義隱式轉換doubleToInt後再重複之前的操作:

scala> implicit def doubleToInt(d: Double) = d.toInt
doubleToInt: (d: Double)Int

scala> val i: Int = 3.5
i: Int = 3

我們看到Double已經被隱式的轉換為我們期望的Int型別。
2.轉換接收者
如果你編寫了some.doSomething()的程式碼,而 some中沒有定義doSomething方法的話,編譯器在放棄之前會嘗試插入轉換程式碼。

我們通過一個有理數例子來看看這種轉換:

package layne.blog.impilcit

/**
  * Created by layne on 08/05/17.
  */
class Rational(val numerator: Int, val denominator: Int) {

  require(denominator != 0) //分母為零

  def +(rational: Rational): Rational = {
    /**
      * 最大公約數
      *
      * @param numerator   分子
      * @param denominator 分母
      * @return 最大公約數
      */
    def divisor(numerator: Int, denominator: Int): Int = {
      val s = if (numerator > denominator) denominator else numerator
      (1 to s).reverse.find(it => numerator % it == 0 && denominator % it == 0).getOrElse(1)
    }
    val d = divisor(rational.numerator * this.denominator + this.numerator * rational.denominator,
      this.denominator * rational.denominator)
    Rational((rational.numerator * this.denominator + this.numerator * rational.denominator) / d, this.denominator * rational.denominator / d)
  }

  override def toString: String = this.numerator + "/" + this.denominator
}

object Rational {

  def apply(numerator: Int, denominator: Int): Rational = new Rational(numerator, denominator)

//隱式轉換整數為Rational
  implicit def intToRational(num: Int) = new Rational(num, 1)
}

object TestMain extends App {
  val oneHalf =  Rational(1, 2)
  val rational = 2 + oneHalf
  println(rational)
}

輸出結果為5/2
如果沒有隱式轉換 們將很難實現 如:2 + oneHalf 的表示式計算,Int型別本身並沒有簽名為: +(rational: Rational): Rational的方法。而通過隱式轉換方法呼叫者,很輕鬆完成。

3.作為隱式引數

呼叫一個帶有隱式引數的函式,如果我們不提供顯式引數,編譯器不會馬上報錯,在報錯之前,編譯器會嘗試在作用域內尋找單一隱式值(用implicit關鍵字修飾的引數),用來補充缺失引數:
下例中首先定義一個帶隱式引數的函式,我們嘗試呼叫,作用域內沒有可以用來補充缺失引數的隱式值

scala> def test(implicit t: String): Unit = println(t)
test: (implicit t: String)Unit
scala> test
<console>:9: error: could not find implicit value for parameter t: String
              test
              ^

新增一個隱式值,再呼叫

scala> implicit val s:String = "test"
s: String = test

scala> test
test
scala> test("test")
test

作用域內有多個匹配的隱式值:

scala> implicit val s1:String = "test"
s1: String = test

scala> test
<console>:11: error: ambiguous implicit values:
 both value s of type => String
 and value s1 of type => String
 match expected type String
              test
              ^

再來看一個稍微複雜一點,用來選出集合內的最大元素的例子:

package layne.blog.impilcit

import scala.concurrent.duration.Deadline

/**
  * Created by layne on 09/05/17.
  */
object MaxElement {

  def maxElement[T <: Ordered[T]](list: List[T]): T = list match {
    case List() => throw new RuntimeException("空集合")
    case List(x) => x
    case x :: rest => val maxRest = maxElement(rest)
      if (x > maxRest) x else maxRest
  }
  def maxElement2[T](list: List[T])(implicit covert: T => Ordered[T]): T = list match {
    case List() => throw new RuntimeException("空集合")
    case List(x) => x
    case x :: rest => val maxRest: T = maxElement2(rest)(covert)
      if (covert(x) > maxRest) x else maxRest
  }

  def maxElement3[T <% Ordered[T]](list: List[T]): T = list match {
    case List() => throw new RuntimeException("空集合")
    case List(x) => x
    case x :: rest => val maxRest: T = maxElement3(rest)
      if (x > maxRest) x else maxRest
  }

  def main(args: Array[String]): Unit = {
    val e1 = Deadline.now
    Thread.sleep(1000)
    val e2 = Deadline.now
    Thread.sleep(1000)
    val e3 = Deadline.now
    println(maxElement(List(e1, e2, e3)))
    println(maxElement2(List("e1", "e2", "e3")))
    println(maxElement2(List(1, 2, 3)))
  }
}

為了能方便的使用 “>”,”<”等操作符,我們讓集合內的元素T必須是Ordered[T] 的子類,
但是這樣做,我們的大多數基本型別都用不了了,因為scala的基本型別不是Ordered[T]的直接子類。
例如我們如果呼叫:println(maxElement(List("e1", "e2", "e3"))), 編譯時就會有如下錯誤

Error:(39, 13) inferred type arguments [String] do not conform to method maxElement's type parameter bounds [T <: Ordered[T]]
    println(maxElement(List("e1", "e2", "e3")))
Error:(39, 28) type mismatch;
 found   : List[String]
 required: List[T]
    println(maxElement(List("e1", "e2", "e3")))

於是我們想到了隱式引數,maxElement2,因為implicit修飾的是整個引數列表,所以我們需要用柯里化將顯示引數和隱式引數分開,scala標準庫提供了許多T => Ordered[T]的隱式轉換,如:定義在scala.Predef中的implicit def augmentString(x: String): StringOps = new StringOps(x) 讓我們能將maxElement2運用在String型別上
因此,我們能把maxElement2,用在許多型別上。

maxElement2也是第三個函式maxElement3中檢視界定的原理,不熟悉的小夥伴們可以仔細揣摩一下。

3.隱式類

所謂隱式類就是用implicit 關鍵字修飾的類(隱式類與樣例類互斥即一個類不能同時被implicit和case修飾)
例如:

package layne.blog.impilcit

/**
  * Created by layne on 09/05/17.
  */
object ImplicitClass {
  implicit class ImplString(string: String) {
    def sayHellow = println("hello " + string)
  }

}
object TestMain{
  def main(args: Array[String]): Unit = {
    def main(args: Array[String]): Unit = {
      import layne.blog.impilcit.ImplicitClass._
      "jack".sayHellow
    }
  }
}

當 “jack”.sayHellow 時,scala 編譯器不會立馬報錯,而檢查當前作用域有沒有 用implicit 修飾的,同時可以將String作為引數的構造器,並且具有方法sayHellow的類,經過查詢 發現 ImplString 符合要求
利用隱式類 ImplString 執行sayHellow 方法

有了這樣的隱式類,我們還能模擬新語法,例如我們建立Map物件:

Map(1 -> "test1",2 -> "test2")

” -> ” 這不是scala內建的語法,只是定義在scala.Predef中的類ArrowAssoc中的方法,感興趣的同學可以去研究下。

隱式操作的頻繁使用會讓程式碼變得晦澀難懂,但是我們要學習scala,乃至檢視用scala編寫的開源專案如spark,kafka,我們必須熟練這個特性。