1. 程式人生 > 程式設計 >Scala implicit 隱式轉換安全駕駛指南

Scala implicit 隱式轉換安全駕駛指南

這篇短文將結合例項對隱式轉換的各種場景進行解釋和總結,希望看完的人能夠安全駛過隱式轉換這個大坑。

隱式轉換函式

隱式轉換函式有兩種作用場景。

  • 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 因為型別不匹配,這段本來是無法通過編譯的,但是編譯器發現存在一個Double至Int的隱式轉換函式,所以進行了隱式轉換。

"Teddy".bark() String型別本來是沒有bark方法的,但是編譯器發現了隱式轉換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) // 正確!複製程式碼

雖然我們可以建立帶有多個非隱式引數的隱式類,但這些類無法用於隱式轉換。

  • 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型別的隱式值,便將其傳入,相當於執行了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)的時候,編譯器發現需要一個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 的含義是T可以被隱式轉換成Ordered[T],這也是為什麼`x > y`可以編譯通過。%>

上面的寫法其實等同於下面這樣,所以視界的語法不能用了也不要緊。

  def min[T](x: T,y: T)(implicit c: T => Ordered[T]): T = {
    if (x > y) y else x
  }複製程式碼

隱式轉換機制

隱式轉換通用規則

  • 標記規則:只有標記為implicit的定義才是可用的。
  • 作用域規則:插入的隱式轉換必須以單一識別符號的形式處於作用域中,或與轉換的源或目標型別關聯在一起。

單一識別符號意思是不能插入形式為someVariable.convert(x)的轉換,只能是convert(x)。單一識別符號規則有個例外,編譯器還將在源型別或轉換的期望目標型別的伴生物件中尋找隱式定義。

有點難理解?看個例子!

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自帶技能。

  • 無歧義規則:隱式轉換唯有不存在其他轉換的前提下有效。
  • 單一呼叫規則:只會嘗試一個隱式操作。
  • 顯示操作先行規則:若編寫的程式碼型別檢查無誤,則不會嘗試隱式操作。

轉換時機

  • 當型別與目標型別不一致時
  • 當物件呼叫類中不存在的方法或成員時
  • 缺少隱式引數時

也即是能用到隱式操作的有三個地方:轉換為期望型別、指定(方法)呼叫者的轉換、隱式引數。

轉換機制

這段來自深入理解Scala的隱式轉換

即編譯器是如何查詢到缺失資訊的,解析具有以下兩種規則:

  • 1.首先會在當前程式碼作用域下查詢隱式實體(隱式方法 隱式類 隱式物件)
  • 2.如果第一條規則查詢隱式實體失敗,會繼續在隱式引數的型別的作用域裡查詢
    型別的作用域是指與該型別相關聯的全部伴生模組,一個隱式實體的型別T它的查詢範圍如下:
    • 1 如果T被定義為T with A with B with C,那麼A,B,C都是T的部分,在T的隱式解析過程中,它們的伴生物件都會被搜尋
    • 2 如果T是引數化型別,那麼型別引數和與型別引數相關聯的部分都算作T的部分,比如List[String]的隱式搜尋會搜尋List的伴生物件和String的伴生物件
    • 3 如果T是一個單例型別p.T,即T是屬於某個p物件內,那麼這個p物件也會被搜尋
    • 4 如果T是個型別注入S#T,那麼S和T都會被搜尋

上路前的話

這段話來自《Scala程式設計》

隱式操作若過於頻繁使用,會讓程式碼變得晦澀難懂。因此,在考慮新增新的隱式轉換之前,請首先自問是否能夠通過其他手段,諸如繼承、混入組合或方法過載,達到同樣的目的。如果所有這些都不能成功,並且你感覺程式碼仍有一些繁複和冗餘,那麼隱式操作或許正好能幫到你。

所以。。。謹慎使用,小心翻車,good luck!

參考文獻

IMPLICIT CLASSES

scala的隱式轉換學習總結(詳細)

《Scala程式設計》

深入理解Scala的隱式轉換

本文程式碼

Github倉庫

轉載請註明原文地址:liam-blog.ml/2019/09/28/…