Scala 隱式(implicit)詳解


文章正文

通過隱式轉換,程序員可以在編寫Scala程序時故意漏掉一些信息,讓編譯器去嘗試在編譯期間自動推導出這些信息來,這種特性可以極大的減少代碼量,忽略那些冗長,過於細節的代碼。

1、Spark 中的隱式思考

隱式轉換是Scala的一大特性, 如果對其不是很了解, 在閱讀Spark代碼時候就會很迷糊,有人這樣問過我?

RDD這個類沒有reduceByKey,groupByKey等函數啊,並且RDD的子類也沒有這些函數,但是好像PairRDDFunctions這個類里面好像有這些函數 為什么我可以在RDD調用這些函數呢?

答案就是Scala的隱式轉換; 如果需要在RDD上調用這些函數,有兩個前置條件需要滿足:

  • 首先rdd必須是RDD[(K, V)], 即pairRDD類型
  • 需要在使用這些函數的前面Import org.apache.spark.SparkContext._;否則就會報函數不存在的錯誤;

參考SparkContext Object, 我們發現其中有上10個xxToXx類型的函數:

 implicit def intToIntWritable(i: Int) = new IntWritable(i)    
 implicit def longToLongWritable(l: Long) = new LongWritable(l)    
 implicit def floatToFloatWritable(f: Float) = new FloatWritable(f)
 implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
      (implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null) = {
    new PairRDDFunctions(rdd)
 }

這么一組函數就是隱式轉換,其中rddToPairRDDFunctions,就是實現:隱式的將RDD[(K, V)]類型的rdd轉換為PairRDDFunctions對象,從而可以在原始的rdd對象上 調用reduceByKey之類的函數;類型隱式轉換是在需要的時候才會觸發,如果我調用需要進行隱式轉換的函數,隱式轉換才會進行,否則還是傳統的RDD類型的對象;

還說一個弱智的話,這個轉換不是可逆的;除非你提供兩個隱式轉換函數; 這是你會說,為什么我執行reduceByKey以后,返回的還是一個rdd對象呢? 這是因為reduceByKey函數 是PairRDDFunctions類型上面的函數,但是該函數會返回一個rdd對象,從而在用戶的角度無法感知到PairRDDFunctions對象的存在,從而精簡了用戶的認識, 不知曉原理的用戶可以把reduceByKey,groupByKey等函數當着rdd本身的函數

上面是對spark中應用到隱式類型轉換做了分析,下面我就隱式轉換進行總結;

從一個簡單例子出發,我們定義一個函數接受一個字符串參數,並進行輸出

def func(msg:String) = println(msg)

這個函數在func("11")調用時候正常,但是在執行func(11)或func(1.1)時候就會報error: type mismatch的錯誤. 這個問題很好解決

  • 針對特定的參數類型, 重載多個func函數,這個不難, 傳統JAVA中的思路, 但是需要定義多個函數
  • 使用超類型, 比如使用AnyVal, Any;這樣的話比較麻煩,需要在函數中針對特定的邏輯做類型轉化,從而進一步處理

上面兩個方法使用的是傳統JAVA思路,雖然都可以解決該問題,但是缺點是不夠簡潔;在充滿了語法糖的Scala中, 針對類型轉換提供了特有的implicit隱式轉化的功能;

隱式轉化是一個函數, 可以針對一個變量在需要的時候,自動的進行類型轉換;針對上面的例子,我們可以定義intToString函數

implicit def intToString(i:Int)=i.toString

此時在調用func(11)時候, scala會自動針對11進行intToString函數的調用, 從而實現可以在func函數已有的類型上提供了新的類型支持,這里有幾點要說一下:

  • 隱式轉換的核心是from類型和to類型, 至於函數名稱並不重要;上面我們取為intToString,只是為了直觀, int2str的功能是一樣的;隱式轉換函數只關心from-to類型之間的匹配 比如我們需要to類型,但是提供了from類型,那么相應的implicit函數就會調用
  • 隱式轉換只關心類型,所以如果同時定義兩個隱式轉換函數,from/to類型相同,但是函數名稱不同,這個時候函數調用過程中如果需要進行類型轉換,就會報ambiguous二義性的錯誤, 即不知道使用哪個隱式轉換函數進行轉換

上面我們看到的例子是將函數的參數從一個類型自動轉換為一個類型的例子,在Scala中, 除了針對函數參數類型進行轉換以外,還可以對函數的調用者的類型進行轉換.

比如A+B,上面我們談到是針對B進行類型自動轉換, 其實可以在A上做類型轉換,下面我們拿一個例子來說明

class IntWritable(_value:Int){
  def value = _value
  def +(that:IntWritable): IntWritable ={
    new IntWritable(that.value + value)
  }
}
implicit  def intToWritable(int:Int)= new IntWritable(int)
new IntWritable(10) + 10

上面我們首先定義了一個類:IntWritable, 並為int提供了一個隱式類型轉換intToWritable, 從而可以使得IntWritable的+函數在原先只接受IntWritable類型參數的基礎上, 接受一個Int類型的變量進行運算,即new IntWritable(10) + 10可以正常運行

現在換一個角度將"new IntWritable(10) + 10" 換為"10 + new IntWritable(10)"會是什么結果呢?會報錯誤嗎?

按道理是應該報錯誤,首先一個Int內置類型的+函數,沒有IntWritable這個參數類型; 其次,我們沒有針對IntWritable類型提供到Int的隱式轉換, 即沒有提供writableToInt的implicit函數.

但是結果是什么?10 + new IntWritable(10)的是可以正常運行的,而且整個表達的類型為IntWritable,而不是Int, 即Int的10被intToWritable函數隱式函數轉換為IntWritable類型;

結論:隱式轉換可以針對函數參數類型和函數對象進行類型轉換; 現在問題來了,看下面的例子

implicit  def intToWritable(int:Int)= new IntWritable(int)
implicit  def writableToInt(that:IntWritable)=that.value

val result1 = new IntWritable(10) + 10
val result2 = 10 + new IntWritable(10)

在上面的IntWritable類的基礎上,我們提供了兩個隱式類型轉換函數, 即Int和IntWritable之間的雙向轉換;這樣的情況下result1和result2兩個變量的類型是什么?

答案:result1的類型為IntWritable, result2的類型Int;很好理解, result1中的Int類型的10被intToWritable隱式轉換為IntWritable;而result2中的IntWritable(10)被writableToInt 隱式轉換為Int類型;

你肯定會問?result2中為什么不是像上面的例子一樣, 把Int類型的10隱式轉換為IntWritable類型呢?原因就是隱式轉換的優先級;

發生類型不匹配的函數調用時, scala會嘗試進行類型隱式轉換;首先優先進行函數參數的類型轉換,如果可以轉換, 那么就完成函數的執行; 否則嘗試去對函數調用對象的類型進行轉換; 如果兩個嘗試都失敗了,就會報方法不存在或者類型不匹配的錯誤;

OK, Scala的隱式轉換是Scala里面隨處可見的語法, 在Spark中也很重要, 這里對它的講解,算是對Shuffle做一個補充了, 即一個RDD之所以可以進行基於Key的Shuffle操作 是因為RDD被隱式轉換為PairRDDFunctions類型。

2、Scala 隱式使用方式

1.將方法或變量標記為implicit
2.將方法的參數列表標記為implicit
3.將類標記為implicit

Scala支持兩種形式的隱式轉換:
隱式值:用於給方法提供參數
隱式視圖:用於類型間轉換或使針對某類型的方法能調用成功

2.1 隱式值

例1:聲明person方法。其參數為name,類型String


scala> def person(implicit name : String) = name //name為隱式參數 person: (implicit name: String)String

直接調用person方法

scala> person
<console>:9: error: could not find implicit value for parameter name: String
              person
              ^
報錯!編譯器說無法為參數name找到一個隱式值
定義一個隱式值后再調用person方法
scala> implicit val p = "mobin"   //p被稱為隱式值
p: String = mobin
scala> person
res1: String = mobin
因為將p變量標記為implicit,所以編譯器會在方法省略隱式參數的情況下去搜索作用域內的隱式值作為缺少參數。
但是如果此時你又在REPL中定義一個隱式變量,再次調用方法時就會報錯
scala> implicit val p1 = "mobin1"
p1: String = mobin1
scala> person
<console>:11: error: ambiguous implicit values:
 both value p of type => String
 and value p1 of type => String
 match expected type String
              person
              ^

匹配失敗,所以隱式轉換必須滿足無歧義規則,在聲明隱式參數的類型是最好使用特別的或自定義的數據類型,不要使用Int,String這些常用類型,避免碰巧匹配

2.2 隱式視圖

隱式轉換為目標類型:把一種類型自動轉換到另一種類型

例2:將整數轉換成字符串類型:

scala> def foo(msg : String) = println(msg)
foo: (msg: String)Unit
 
scala> foo(10)
<console>:11: error: type mismatch;
found : Int(10)
required: String
foo(10)
^

顯然不能轉換成功,解決辦法就是定義一個轉換函數給編譯器將int自動轉換成String

scala> implicit def intToString(x : Int) = x.toString
intToString: (x: Int)String
 
scala> foo(10)
10

隱式轉換調用類中本不存在的方法

例3:通過隱式轉換,使對象能調用類中本不存在的方法

class SwingType{
  def  wantLearned(sw : String) = println("兔子已經學會了"+sw)
}
object swimming{
  implicit def learningType(s : AminalType) = new SwingType
}
class AminalType
object AminalType extends  App{
  import com.mobin.scala.Scalaimplicit.swimming._
  val rabbit = new AminalType
    rabbit.wantLearned("breaststroke")         //蛙泳
}
編譯器在rabbit對象調用時發現對象上並沒有wantLearned方法,此時編譯器就會在作用域范圍內查找能使其編譯通過的隱式視圖,找到learningType方法后,編譯器通過隱式轉換將對象轉換成具有這個方法的對象,之后調用wantLearned方法
可以將隱式轉換函數定義在伴生對象中,在使用時導入隱式視圖到作用域中即可(如例4的learningType函數)
還可以將隱式轉換函數定義在凶對象中,同樣在使用時導入作用域即可,如例4
例4:
class SwingType{
  def  wantLearned(sw : String) = println("兔子已經學會了"+sw)
}

package swimmingPage{
object swimming{
  implicit def learningType(s : AminalType) = new SwingType  //將轉換函數定義在包中
  }
}
class AminalType
object AminalType extends  App{
  import com.mobin.scala.Scalaimplicit.swimmingPage.swimming._  //使用時顯示的導入
  val rabbit = new AminalType
    rabbit.wantLearned("breaststroke")         //蛙泳
}

像intToString,learningType這類的方法就是隱式視圖,通常為Int => String的視圖,定義的格式如下:

implicit def originalToTarget (<argument> : OriginalType) : TargetType

其通常用在於以兩種場合中:

1.如果表達式不符合編譯器要求的類型,編譯器就會在作用域范圍內查找能夠使之符合要求的隱式視圖。如例2,當要傳一個整數類型給要求是字符串類型參數的方法時,在作用域里就必須存在Int => String的隱式視圖
2.給定一個選擇e.t,如果e的類型里並沒有成員t,則編譯器會查找能應用到e類型並且返回類型包含成員t的隱式視圖。如例3

2.3 隱式類

在scala2.10后提供了隱式類,可以使用implicit聲明類,但是需要注意以下幾點:
1.其所帶的構造參數有且只能有一個
2.隱式類必須被定義在類,伴生對象和包對象里
3.隱式類不能是case class(case class在定義會自動生成伴生對象與2矛盾)
4.作用域內不能有與之相同名稱的標示符

object Stringutils {
  implicit class StringImprovement(val s : String){   //隱式類
      def increment = s.map(x => (x +1).toChar)
  }
}
object  Main extends  App{
  import com.mobin.scala.implicitPackage.Stringutils._
  println("mobin".increment)
}

編譯器在mobin對象調用increment時發現對象上並沒有increment方法,此時編譯器就會在作用域范圍內搜索隱式實體,發現有符合的隱式類可以用來轉換成帶有increment方法的StringImprovement類,最終調用increment方法。

3、Scala 隱私注意事項

3.1 轉換時機

1.當方法中的參數的類型與目標類型不一致時
2.當對象調用類中不存在的方法或成員時,編譯器會自動將對象進行隱式轉換

3.2 解析機制

即編譯器是如何查找到缺失信息的,解析具有以下兩種規則:

1.首先會在當前代碼作用域下查找隱式實體(隱式方法  隱式類 隱式對象)
2.如果第一條規則查找隱式實體失敗,會繼續在隱式參數的類型的作用域里查找
類型的作用域是指與該類型相關聯的全部伴生模塊,一個隱式實體的類型T它的查找范圍如下:
    (1)如果T被定義為T with A with B with C,那么A,B,C都是T的部分,在T的隱式解析過程中,它們的伴生對象都會被搜索
    (2)如果T是參數化類型,那么類型參數和與類型參數相關聯的部分都算作T的部分,比如List[String]的隱式搜索會搜索List的
伴生對象和String的伴生對象
    (3) 如果T是一個單例類型p.T,即T是屬於某個p對象內,那么這個p對象也會被搜索
    (4) 如果T是個類型注入S#T,那么S和T都會被搜索

3.3 轉換前提

1.不存在二義性(如例1)

2.隱式操作不能嵌套使用,即一次編譯只隱式轉換一次(One-at-a-time Rule)

Scala不會把 x + y 轉換成 convert1(convert2(x)) + y

3.代碼能夠在不使用隱式轉換的前提下能編譯通過,就不會進行隱式轉換。

文章參考

  • https://github.com/ColZer/DigAndBuried/blob/master/spark/scala-implicit.md
  • https://blog.csdn.net/jameshadoop/article/details/52337949
  • https://www.cnblogs.com/MOBIN/p/5351900.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM