Scala 系列(十三)—— 隱式轉換和隱式參數

1、隱式轉換

1.1 使用隱式轉換

隱式轉換指的是以 implicit 關鍵字聲明帶有單個參數的轉換函數,它將值從一種類型轉換爲另外一種類型,以便使用以前類型所沒有的功能。示例以下:java

// 普通人
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() 方法的時候,普通人就仍是普通人。一般程序會在如下狀況下嘗試執行隱式轉換:git

  • 當對象訪問一個不存在的成員時,即調用的方法不存在或者訪問的成員變量不存在;
  • 當對象調用某個方法,該方法存在,可是方法的聲明參數與傳入參數不匹配時。

而在如下三種狀況下編譯器不會嘗試執行隱式轉換:github

  • 若是代碼可以在不使用隱式轉換的前提下經過編譯,則不會使用隱式轉換;
  • 編譯器不會嘗試同時執行多個轉換,好比 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()
複製代碼

其次再解釋一下多個轉換的問題:bash

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 引入隱式轉換

隱式轉換的能夠定義在如下三個地方:app

  • 定義在原類型的伴生對象中;
  • 直接定義在執行代碼的上下文做用域中;
  • 統必定義在一個文件中,在使用時候導入。

上面咱們使用的方法至關於直接定義在執行代碼的做用域中,下面分別給出其餘兩種定義的代碼示例:ide

定義在原類型的伴生對象中函數

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 查看所有隱式轉換函數。this


2、隱式參數

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) 中參數 12 其實是經過定義在 Predef 中的隱式轉換方法 intWrapper 轉換爲 RichInt

// Predef.scala
@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)
複製代碼

爲何要這麼麻煩執行隱式轉換,緣由是 Scala 中的 Int 類型並不能直接進行比較,由於其沒有實現 Ordered 特質,真正實現 Ordered 特質的是 RichInt

https://github.com/heibaiying

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 } 複製代碼

參考資料

  1. Martin Odersky . Scala 編程 (第 3 版)[M] . 電子工業出版社 . 2018-1-1
  2. 凱.S.霍斯特曼 . 快學 Scala(第 2 版)[M] . 電子工業出版社 . 2017-7

更多大數據系列文章能夠參見 GitHub 開源項目大數據入門指南

相關文章
相關標籤/搜索