Scala 隱式(implicit)詳解

文章正文

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

一、Spark 中的隱式思考

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

RDD這個類沒有reduceByKey,groupByKey等函數啊,而且RDD的子類也沒有這些函數,可是好像PairRDDFunctions這個類裏面好像有這些函數 爲何我能夠在RDD調用這些函數呢?程序員

答案就是Scala的隱式轉換; 若是須要在RDD上調用這些函數,有兩個前置條件須要知足:github

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

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

 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自己的函數oop

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

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

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

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

  • 針對特定的參數類型, 重載多個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類型。

二、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方法。

三、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
相關文章
相關標籤/搜索