本文為《快學 Scala》以及《深入了解 Scala》的讀書筆記
1 概述
Scala 的隱式轉換系統提供了一套良好的查找機制,可以讓編譯器能夠調整代碼,也就是說即使在寫代碼的時候故意漏掉一些信息,也可以讓編譯器嘗試在編譯期自動推導出來。Scala 編譯器可以推導以下兩種情況:
缺少參數的方法調用或構造器調用。
缺少了從一種類型到另一種類型的轉換。
要理解隱式轉換,首先要了解作用域的問題。然后是隱式解析的規則。
2 隱式參數和隱式轉換
程序員應該謹慎明智地使用隱式對象,濫用隱式會讓讀者無法理解代碼的。執行上下文是隱式的一個不錯的使用場景,在編寫事務、數據庫連接、線程池以及用戶會話時,也是不錯的場景。這種使用方法可以讓 API
更加簡潔。
2.1 繞開類型擦除帶來的限制
JVM 忘記了為參數化類型提供類型參數。編譯器是不允許同時出現以下方法的定義,但是我們可以通過隱式參數來解決這個問題。
// 在 REPL 上單獨命名兩個方法是可以的,但是用 :paste 模式就不行了
object M {
def m(seq: Seq[Int]): Unit = println(s"Seq[Int]: $seq")
def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")
}
// 解決方法
// m 可以是兩種不同的類型,編譯都可以通過
object M {
implicit object IntMarker
implicit object StringMarker
def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit = println(s"Seq[Int]: $seq")
def m(seq: Seq[String])(implicit i:StringMarker.type): Unit = println(s"Seq[String]: $seq")
}
import M._
m(List(1, 2, 3)
m(List("one", "two", "three")
隱式參數,是一個單參數的函數
隱式轉換,用于在類型之間做轉換
隱式轉換函數是指那種以 implicit
關鍵字聲明的帶有單個參數的函數。這樣的函數被自動應用,將值從一種類型轉換為另一種類型。
// 如果是 Int 類型,則會為自動應用 int2Fraction 這個隱式轉換函數
implicit def int2Fraction(n: Int) = Fraction(n, 1)
val result = 3 * Fraction(4, 5)
可以利用隱式轉換豐富現有類庫的功能
# File 對象被隱式轉換為 RichFile
class RichFile(val from: File) {
def read = Source.fromFile(from.getPath).mkString
}
implicit def file2RichFile(from: File) = new RichFile(from)
Scala 會考慮如下的隱式轉換函數
位于源或目標類型的伴生對象中的隱式函數
位于當前作用域可以以單個標識符指代的隱式函數
# 注意引入自定義的隱式轉換函數的處理方法
import com.horsemann.impatient.FractionConversions
# 這個才可以將隱式方法本身傳入
import com.horsemann.impatient.FractionConversions._
由于以上方法引入隱式轉換函數具有全句化,為了避免這種問題,應該采取局部化。
# 避免某個特定的隱式轉換帶來麻煩
import com.horsemann.impatient.FractionConversions.{fraction2Doubel => _, _}
如果想知道編譯器用了哪些隱式轉換,可以用如下命令行參數來編譯程序:
scalac -Xprint:typer MyProg.scala
隱式參數,函數或者方法可以帶有一個標記為 implicit
的參數列表。這種情況下,編譯器將會去找缺省值,提供給該函數或方法。
case class Delimiters(left: String, right: String)
def quote(what: String)(implicit delims: Delimiters) =
delims.left + what + delims.right
# 可以顯式的 Delimiters 對象來調用 quote 方法
quote("Bonjour le monde")
缺省情況下編譯器會查找一個類型為 Delimiters
的隱式值,這必須是一個被聲明為 implicit
的值
隱式的函數參數也可以被用作隱式轉換。
# 編譯報錯,因為并不知道 a 或者 b 是屬于一個帶有 < 操作符的類型
def smaller[T](a: T, b:T) = if (a < b) a else b
# 可以提供一個轉換函數來達到目的
def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) =
if (order(a) < b) a else b
2.2 典型的隱式轉換
定義 -> 方法的封裝對象,Scala 已經在 Predef 對象中定義了該對象:
implicit final class ArrowAssoc[A](val self: A) {
def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
}
能夠執行隱式轉換的無外乎兩類:構造方法只接受單一參數的類型或者只接受單一參數的方法。
2.3 Scala 內置的各種隱式
Scala 2.11 版庫源代碼中定義了超過300個隱式方法、隱式值和隱式類型。
Scala 的數值對象擁有大量的可以用于轉換類型的伴生對象及隱式方法。
scala> val b = 1
b: Int = 1
scala> Big
BigDecimal BigInt
scala> BigInt(b)
res2: scala.math.BigInt = 1
Predef 中定義了大多數的隱式定義。其中的一些隱式定義包含了 @inline
標注。這一標注鼓勵編譯器努力嘗試將函數調用內聯 inline
,以減少棧幀的開銷。與之對應的是 @noinline
標注。
一些方法能將某一類型轉化為另一類型,例如將某一類型封裝成新的類型后,新的類型就會提供新的方法。
出于性能的考慮,Scala 會盡可能避免執行類型轉換。不過因為目標集合的抽象體是建立在底層容器的基礎上,因此并不會造成太多的性能消耗。