1. 程式人生 > >【Scala型別系統】隱式轉換與隱式引數

【Scala型別系統】隱式轉換與隱式引數

隱式轉換

隱式轉換是使用implicit修飾的帶有單個引數的普通函式。這種函式將自動應用,將值從一種型別轉換為另一種型別。
舉例說明:

我們想將整數n轉換為分數n/1,
定義implicit def int2Fraction(n: Int) = Fraction(n, 1)
在進行如下表達式求值的時候:
val result = 3 * Fraction(4, 5)
編譯器將呼叫int2Fraction(3),將整數轉換成一個Fraction物件,然後按照Fraction類的*方法定義來進行計算。

隱式轉換可以為現有的類庫新增功能:

我們想為java.io.File類新增一個read方法來讀取檔案
在Scala中,可以定義一個型別來提供read或你想要的功能:

class RichFile(val from: File) {
  def read = Source.fromFile(from.getPath).mkString
}

然後再提供一個隱式轉換來將原來的File型別轉換到新定義的型別:

implicit def file2RichFile(from: File) = new RichFile(from)

這樣就可以在File物件上呼叫read方法了,它被隱式轉換為一個RichFile物件。

隱式轉換的規則

作用域規則

隱式轉換函式的可用位置:

  1. 位於源或目標型別的伴生物件中的隱式函式
  2. 位於當前作用域可以以單個識別符號指代的隱式函式

假如隱式函式放在了Conversions物件中,而這個物件位於com.xxx.yyy包,那麼引入語句應該是:import com.xxx.yyy.Conversions._

無歧義規則

隱式轉換的應用場景:

  • 當表示式的型別與預期的型別不同時:
    比如sqrt(Fraction(1, 4))中,sqrt期望一個Double的引數,編譯器會呼叫類似fraction2Double的隱式函式來將Fraction物件轉換為Double物件。
  • 當物件訪問一個不存在的成員時:
    比如new File("README").read中,File沒有read方法,編譯器會呼叫file2RichFile,然後呼叫RichFile中定義的read方法。
  • 當物件呼叫某個方法,而該方法的引數宣告與傳入引數不匹配時:
    比如3 * Fraction(4, 5)中,Int類的*方法不接收Fraction作為引數,編譯器會呼叫int2Fraction進行隱式轉換。

編譯器不會使用隱式轉換的情況:

  • 如果程式碼能夠不適用隱式轉換的前提下通過編譯,則不會使用隱式轉換。
  • 編譯器不會嘗試同時執行多個轉換,所以隱式轉換是單一呼叫的。
  • 存在二義性的轉換是錯誤的,編譯器將會報錯。

隱式引數

函式或方法可以帶有一個標記為implicit的引數列表。該情況下,編譯器會查詢預設值,提供給該函式或方法。
比如:

case class Delimiters(left: String, right: String)

def quote(what: String)(implicit delims: Delimiters) =
  delims.left + what + delims.right

implicit val quoteDelimiters = Delimiters("<<", ">>")

當我們呼叫quote("Scala Programming")時,編譯器會查詢一個型別為Delimiters的隱式值,輸出<<Scala Programming>>

編譯器的查詢位置:

  • 當前作用域所有可用單個識別符號指代的滿足型別要求的val和def
  • 與所要求型別相關聯的型別的繁盛物件。相關聯的型別包括所要求型別本身,以及它的型別引數。

利用隱式引數進行隱式轉換

我們提供一個泛型函式來得到相對小的值:
def smaller[T](a: T, b: T) = if (a < b) a else b
這裡由於我們並不知道a和b的型別是否有<操作符,所以編譯器不會通過。
解決辦法是新增一個隱式引數order來指代一個轉換函式:

def smaller(a: T, b: T)(implicit order: T => Ordered[T])
  = if(order(a) < b) a else b

由於Ordered[T]特質中有一個接受T作為引數的<方法,所以編譯器將在編譯時知道T,並且從而判決是否T => Ordered[T]型別的隱式定義存在於作用域中。
這樣,才可以呼叫smaller(40, 2)或者smaller("AA", "BB")
注意,order是一個帶有單個引數的函式,被打上了implicit標籤,所以它不僅是一個隱式引數,也是一個隱式轉換。那麼,我們可以在函式體重省略order的顯示呼叫。

def smaller[T](a: T, b: T)(implicit order: T => Ordered[T])
  = if (a < b) a else b

因為a沒有帶<的方法,那麼會呼叫order(a)進行轉換。