隱式定義是指編譯器為了修正類型錯誤而允許插入到程序中的定義。
舉例:
正常情況下"120"/12顯然會報錯,因為 String 類並沒有實現 / 這個方法,我們無法去決定 String 類的行為,這個時候就可以用上 implicit 關鍵字了。
使用 implicit 關鍵字定義函數。
implicit def String2Int(str: String) = {
str.toInt
}
print("120" / 12)
編譯器一旦發現對於 String 類操作符 / 不可用,而 + 方法正好對應 Int 型的參數,且當前作用域存在 String 類型的隱式轉換。
所以實際等價於print(String2Int("120") / 12)。
隱式操作規則
作用域規則:插入的隱式轉換必須以單一標識符的形式處於作用域中,或與轉換的源或目標類型關聯在一起。
Scope Rule: An inserted implicit conversion must be in scope as a single identifier, or be associated with the source or target type of the conversion.
Scala 編譯器僅考慮處於作用域之內的隱式轉換。可以使用 import 關鍵字訪問其它庫的隱式轉換。
編譯器還將在源類型或轉化的期望目標類型的伴生對象中尋找隱式定義。
case class Euro(num: Int)
object Dollar {
implicit def EuroToDollar(euro: Euro): Dollar = Dollar(2 * euro.num)
}
case class Dollar(num:Int) {
def +(d: Dollar) = num + d.num
}
print(Dollar(3) + Euro(10)) // 23
無歧義規則:隱式轉換唯有在不存在其它可插入轉換的前提下才能插入。
Non-Ambiguity Rule: An implicit conversion is only inserted if there is no other possible conversion to insert.
implicit def String2Int(str: String) = {
str.toInt
}
implicit def String2Double(str: String) = {
str.toDouble
}
print("120" / 12)
上面的程序會導致編譯錯誤。可以想象,如果不禁止這種操作勢必會導致可讀性的下降。
單一調用原則:只會嘗試一個隱式操作。
One-at-a-time Rule: Only one implicit is tried.
編譯器不會對某個變量執行多次的隱式轉換。
顯式操作先行原則:若編寫的代碼類型檢查無誤,則不會嘗試任何隱式操作。
Explicits-First Rule: Whenever code type checks as it is written, no implicits are attempted.
implicit def String2Int(str: String) = {
str.toInt
}
print("120" + 12) // 12012
120並沒有被轉換為 Int 類型,而是 12 被轉換成了 String 類型。
也就是說編譯器並不會優先考慮我們定義的隱式操作。
命名隱式轉換
Naming an implicit conversion.
object MyConversions {
implicit def String2Int(str: String) = str.toInt
implicit def Double2Int(num: Double) = num.toInt
}
import MyConversions.Double2Int
val x:Int = 12.0
print(x)
這樣可以很方便的在當前作用域內引入我們需要的隱式轉換。
隱式轉換
與新類型的交互操作
現在我們有一個 Person 類。
case class Person(name: String, age: Int) {
def +(x: Int) = age + x
def +(p: Person) = age + p.age
}
其中定義了 + 方法。
val person = Person("xiaohong", 1)
println(person + 1) // 2
但是反過來 println(1 + person) 就不行了。(整數類型顯然沒有合適的 + 方法)
這個時候定義從 Int 到 Person 的隱式轉換就很方便了。
implicit def Add1(x: Int) = Person("Empty", x)
println(1 + person) // 2
當然也可以定義從 Person 到 Int 的隱式轉換。
implicit def Add2(x: Person) = x.age
println(1 + person) // 2
但是如果我們同時定義了這兩個函數,看看會發生什么?
implicit def Add1(x: Int) = Person("Empty", x)
implicit def Add2(x: Person) = x.age + 1 // 加以區分
println(1 + person) // 3
實際上 + 操作的原型是 1.+(person),person 作為參數。所以 person 被轉化成了 Int 類型參與計算。
模擬新的語法
我們有很多種方法去創建一個 Map 對象。
var mp = Map(1 -> 2, 3 -> 4)
var mp2 = Map(Tuple2(1, 2), Tuple2(3, 4))
var mp3 = Map((1, 2), (3, 4))
var mp4 = Map(1 → 2, 3 → 4)
print(mp == mp2)
print(mp == mp3)
print(mp2 == mp3)
print(mp == mp4)
到今天才知道 -> 符號是如何支持這種操作的。
去 Predef 庫中找到 -> 的實現如下:
implicit final class ArrowAssoc[A](private val self: A) extends AnyVal {
@inline def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
def →[B](y: B): Tuple2[A, B] = ->(y)
}
並不是什么內建語法,其實就是基於隱式轉換。
現在自己定義一個符號 -->
implicit class MyRange(start: Int) {
def -->(end: Int) = start to end
}
print((1 --> 10).sum) // 55
隱式參數
在一個方法的參數名前加上 implicit 關鍵字。
implicit val a = 2
implicit val b = "B"
def fun(implicit x: Int, y:String) = {
x + y
}
fun // 2B
fun(1, "A") // 1A
如果我們不提供相應的參數,那么方法會自動帶入當前作用域內的帶有 implicit 關鍵字的變量作為參數。編譯器會根據類型去匹配。
總結
隱式操作是功能強大、代碼凝練的 Scala 特性。無論是標准庫內還是其它開源的框架,都大量的使用了這一特性。但是它的頻繁使用必然會導致代碼可讀性的大幅度降低,當然我針對的是庫的使用者以及兩個月后再看庫的開發者。(越來越感覺 Scala 和 Java 走了兩個極端)
參考
