入門大資料---Scala隱式轉換和隱式引數
一、隱式轉換
1.1 使用隱式轉換
隱式轉換指的是以 implicit
關鍵字宣告帶有單個引數的轉換函式,它將值從一種型別轉換為另一種型別,以便使用之前型別所沒有的功能。示例如下:
// 普通人 class Person(val name: String) // 雷神 class Thor(val name: String) { // 正常情況下只有雷神才能舉起雷神之錘 def hammer(): Unit = { println(name + "舉起雷神之錘") } } object Thor extends App { // 定義隱式轉換方法 將普通人轉換為雷神 通常建議方法名使用 source2Target,即:被轉換物件 To 轉換物件 implicit def person2Thor(p: Person): Thor = new Thor(p.name) // 這樣普通人也能舉起雷神之錘 new Person("普通人").hammer() } 輸出: 普通人舉起雷神之錘
1.2 隱式轉換規則
並不是你使用 implicit
轉換後,隱式轉換就一定會發生,比如上面如果不呼叫 hammer()
方法的時候,普通人就還是普通人。通常程式會在以下情況下嘗試執行隱式轉換:
- 當物件訪問一個不存在的成員時,即呼叫的方法不存在或者訪問的成員變數不存在;
- 當物件呼叫某個方法,該方法存在,但是方法的宣告引數與傳入引數不匹配時。
而在以下三種情況下編譯器不會嘗試執行隱式轉換:
- 如果程式碼能夠在不使用隱式轉換的前提下通過編譯,則不會使用隱式轉換;
- 編譯器不會嘗試同時執行多個轉換,比如
convert1(convert2(a))*b
; - 轉換存在二義性,也不會發生轉換。
這裡首先解釋一下二義性,上面的程式碼進行如下修改,由於兩個隱式轉換都是生效的,所以就存在了二義性:
//兩個隱式轉換都是有效的
implicit def person2Thor(p: Person): Thor = new Thor(p.name)
implicit def person2Thor2(p: Person): Thor = new Thor(p.name)
// 此時下面這段語句無法通過編譯
new Person("普通人").hammer()
其次再解釋一下多個轉換的問題:
class ClassA { override def toString = "This is Class A" } class ClassB { override def toString = "This is Class B" def printB(b: ClassB): Unit = println(b) } class ClassC class ClassD object ImplicitTest extends App { implicit def A2B(a: ClassA): ClassB = { println("A2B") new ClassB } implicit def C2B(c: ClassC): ClassB = { println("C2B") new ClassB } implicit def D2C(d: ClassD): ClassC = { println("D2C") new ClassC } // 這行程式碼無法通過編譯,因為要呼叫到 printB 方法,需要執行兩次轉換 C2B(D2C(ClassD)) new ClassD().printB(new ClassA) /* * 下面的這一行程式碼雖然也進行了兩次隱式轉換,但是兩次的轉換物件並不是一個物件,所以它是生效的: * 轉換流程如下: * 1. ClassC 中並沒有 printB 方法,因此隱式轉換為 ClassB,然後呼叫 printB 方法; * 2. 但是 printB 引數型別為 ClassB,然而傳入的引數型別是 ClassA,所以需要將引數 ClassA 轉換為 ClassB,這是第二次; * 即: C2B(ClassC) -> ClassB.printB(ClassA) -> ClassB.printB(A2B(ClassA)) -> ClassB.printB(ClassB) * 轉換過程 1 的物件是 ClassC,而轉換過程 2 的轉換物件是 ClassA,所以雖然是一行程式碼兩次轉換,但是仍然是有效轉換 */ new ClassC().printB(new ClassA) } // 輸出: C2B A2B This is Class B
1.3 引入隱式轉換
隱式轉換的可以定義在以下三個地方:
- 定義在原型別的伴生物件中;
- 直接定義在執行程式碼的上下文作用域中;
- 統一定義在一個檔案中,在使用時候匯入。
上面我們使用的方法相當於直接定義在執行程式碼的作用域中,下面分別給出其他兩種定義的程式碼示例:
定義在原型別的伴生物件中:
class Person(val name: String)
// 在伴生物件中定義隱式轉換函式
object Person{
implicit def person2Thor(p: Person): Thor = new Thor(p.name)
}
class Thor(val name: String) {
def hammer(): Unit = {
println(name + "舉起雷神之錘")
}
}
// 使用示例
object ScalaApp extends App {
new Person("普通人").hammer()
}
定義在一個公共的物件中:
object Convert {
implicit def person2Thor(p: Person): Thor = new Thor(p.name)
}
// 匯入 Convert 下所有的隱式轉換函式
import com.heibaiying.Convert._
object ScalaApp extends App {
new Person("普通人").hammer()
}
注:Scala 自身的隱式轉換函式大部分定義在
Predef.scala
中,你可以開啟原始檔檢視,也可以在 Scala 互動式命令列中採用:implicit -v
檢視全部隱式轉換函式。
二、隱式引數
2.1 使用隱式引數
在定義函式或方法時可以使用標記為 implicit
的引數,這種情況下,編譯器將會查詢預設值,提供給函式呼叫。
// 定義分隔符類
class Delimiters(val left: String, val right: String)
object ScalaApp extends App {
// 進行格式化輸出
def formatted(context: String)(implicit deli: Delimiters): Unit = {
println(deli.left + context + deli.right)
}
// 定義一個隱式預設值 使用左右中括號作為分隔符
implicit val bracket = new Delimiters("(", ")")
formatted("this is context") // 輸出: (this is context)
}
關於隱式引數,有兩點需要注意:
1.我們上面定義 formatted
函式的時候使用了柯里化,如果你不使用柯里化表示式,按照通常習慣只有下面兩種寫法:
// 這種寫法沒有語法錯誤,但是無法通過編譯
def formatted(implicit context: String, deli: Delimiters): Unit = {
println(deli.left + context + deli.right)
}
// 不存在這種寫法,IDEA 直接會直接提示語法錯誤
def formatted( context: String, implicit deli: Delimiters): Unit = {
println(deli.left + context + deli.right)
}
上面第一種寫法編譯的時候會出現下面所示 error
資訊,從中也可以看出 implicit
是作用於引數列表中每個引數的,這顯然不是我們想要到達的效果,所以上面的寫法採用了柯里化。
not enough arguments for method formatted:
(implicit context: String, implicit deli: com.heibaiying.Delimiters)
2.第二個問題和隱式函式一樣,隱式預設值不能存在二義性,否則無法通過編譯,示例如下:
implicit val bracket = new Delimiters("(", ")")
implicit val brace = new Delimiters("{", "}")
formatted("this is context")
上面程式碼無法通過編譯,出現錯誤提示 ambiguous implicit values
,即隱式值存在衝突。
2.2 引入隱式引數
引入隱式引數和引入隱式轉換函式方法是一樣的,有以下三種方式:
- 定義在隱式引數對應類的伴生物件中;
- 直接定義在執行程式碼的上下文作用域中;
- 統一定義在一個檔案中,在使用時候匯入。
我們上面示例程式相當於直接定義執行程式碼的上下文作用域中,下面給出其他兩種方式的示例:
定義在隱式引數對應類的伴生物件中;
class Delimiters(val left: String, val right: String)
object Delimiters {
implicit val bracket = new Delimiters("(", ")")
}
// 此時執行程式碼的上下文中不用定義
object ScalaApp extends App {
def formatted(context: String)(implicit deli: Delimiters): Unit = {
println(deli.left + context + deli.right)
}
formatted("this is context")
}
統一定義在一個檔案中,在使用時候匯入:
object Convert {
implicit val bracket = new Delimiters("(", ")")
}
// 在使用的時候匯入
import com.heibaiying.Convert.bracket
object ScalaApp extends App {
def formatted(context: String)(implicit deli: Delimiters): Unit = {
println(deli.left + context + deli.right)
}
formatted("this is context") // 輸出: (this is context)
}
2.3 利用隱式引數進行隱式轉換
def smaller[T] (a: T, b: T) = if (a < b) a else b
在 Scala 中如果定義了一個如上所示的比較物件大小的泛型方法,你會發現無法通過編譯。對於物件之間進行大小比較,Scala 和 Java 一樣,都要求被比較的物件需要實現 java.lang.Comparable 介面。在 Scala 中,直接繼承 Java 中 Comparable 介面的是特質 Ordered,它在繼承 compareTo 方法的基礎上,額外定義了關係符方法,原始碼如下:
trait Ordered[A] extends Any with java.lang.Comparable[A] {
def compare(that: A): Int
def < (that: A): Boolean = (this compare that) < 0
def > (that: A): Boolean = (this compare that) > 0
def <= (that: A): Boolean = (this compare that) <= 0
def >= (that: A): Boolean = (this compare that) >= 0
def compareTo(that: A): Int = compare(that)
}
所以要想在泛型中解決這個問題,有兩種方法:
1. 使用檢視界定
object Pair extends App {
// 檢視界定
def smaller[T<% Ordered[T]](a: T, b: T) = if (a < b) a else b
println(smaller(1,2)) //輸出 1
}
檢視限定限制了 T 可以通過隱式轉換 Ordered[T]
,即物件一定可以進行大小比較。在上面的程式碼中 smaller(1,2)
中引數 1
和 2
實際上是通過定義在 Predef
中的隱式轉換方法 intWrapper
轉換為 RichInt
。
// Predef.scala
@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)
為什麼要這麼麻煩執行隱式轉換,原因是 Scala 中的 Int 型別並不能直接進行比較,因為其沒有實現 Ordered
特質,真正實現 Ordered
特質的是 RichInt
。
2. 利用隱式引數進行隱式轉換
Scala2.11+ 後,檢視界定被標識為廢棄,官方推薦使用型別限定來解決上面的問題,本質上就是使用隱式引數進行隱式轉換。
object Pair extends App {
// order 既是一個隱式引數也是一個隱式轉換,即如果 a 不存在 < 方法,則轉換為 order(a)<b
def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b
println(smaller(1,2)) //輸出 1
}
參考資料
- Martin Odersky . Scala 程式設計 (第 3 版)[M] . 電子工業出版社 . 2018-1-1
- 凱.S.霍斯特曼 . 快學 Scala(第 2 版)[M] . 電子工業出版社 . 2017-7