文章正文
通過隱式轉換,程序員可以在編寫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 ^
scala> implicit val p = "mobin" //p被稱為隱式值 p: String = mobin scala> person res1: String = mobin
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") //蛙泳 }
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 解析機制
即編譯器是如何查找到缺失信息的,解析具有以下兩種規則:
3.3 轉換前提
1.不存在二義性(如例1)
2.隱式操作不能嵌套使用,即一次編譯只隱式轉換一次(One-at-a-time Rule)
Scala不會把 x + y 轉換成 convert1(convert2(x)) + y
文章參考
- 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