1. 程式人生 > >Scala 隱式(implicit)詳解

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

相關推薦

Scala implicit

文章正文 通過隱式轉換,程式設計師可以在編寫Scala程式時故意漏掉一些資訊,讓編譯器去嘗試在編譯期間自動推匯出這些資訊來,這種特性可以極大的減少程式碼量,忽略那些冗長,過於細節的程式碼。 1、Spark 中的隱式思考 隱式轉換是Scala的一大特性, 如果對其不是很瞭解, 在閱讀Spark程式碼時候就

馬爾可夫模型HMM

上邊的圖示都強調了 HMM 的狀態變遷。而下圖則明確的表示出模型的演化,其中綠色的圓圈表示隱藏狀態,紫色圓圈表示可觀察到狀態,箭頭表示狀態之間的依存概率,一個 HMM 可用一個5元組 { N, M, π,A,B } 表示,其中 N 表示隱藏狀態的數量,我們要麼知道確切的值,要麼猜測該值,M 表示可觀測狀態的數

機器學習中的馬爾科夫模型HMM

前導性推薦閱讀資料: 歡迎關注白馬負金羈的部落格 http://blog.csdn.net/baimafujinji,為保證公式、圖表得以正確顯示,強烈建議你從該地址上檢視原版博文。本部落格主要關注方向包括:數字影象處理、演算法設計與分析、資料結構、機器學

GoLang基礎數據類型--->字典map

golang ont nbsp spa 數據 否則 創作 聲明 作者                          GoLang基礎數據類型--->字典(map)詳解                                                 

SQL語句之數據定義語言DDL

三種 absolute row redundant 字符 對象 not null 工作 part 操作對象:數據庫 1)創建數據庫 MariaDB [(none)]> help create databaseName: ‘CREATE DATABASE‘Descrip

指標1-- 軌道線指標ENE

本質 平均值 width 簡單 公式 方向 重新 alt 改變 軌道線指標(ENE): 1、定義:軌道線(ENE)由上軌線(UPPER)和下軌線(LOWER)及中軌線(ENE)組成,軌道線的優勢在於其不僅具有趨勢軌道的研判分析作用,也可以敏銳的覺察股價運行過程中方向的改

指標5-- 布林線指標BOLL

tar 線下 pan evel 隨著 向上 log link bsp 一、定義:布林線指標,即BOLL指標,其英文全稱是“Bollinger Bands”,布林線(BOLL)由約翰·布林先生創造,其利用統計原理,求出股價的標準差及其信賴區間,從而確定股價的波動範圍及未來走勢

GoLang基礎資料型別--->字典map

                     GoLang基礎資料型別--->字典(map)詳解                                             作者:尹正傑 版權宣告:原創作品,謝絕轉載!否則將追究法律責任。  

linux 之mysql——約束constraint

一、什麼是約束 約束英文:constraint 約束實際上就是表中資料的限制條件 二、約束作用 表在設計的時候加入約束的目的就是為了保證表中的記錄完整和有效性 比如name欄位中要讓其使用者名稱不重複,這就需要新增約束。或者必須註冊的時候需要新增郵箱等  三、約束種類

c++迭代器iterator【轉】

(轉自:https://www.cnblogs.com/hdk1993/p/4419779.html) 1. 迭代器(iterator)是一中檢查容器內元素並遍歷元素的資料型別。 (1) 每種容器型別都定義了自己的迭代器型別,如vector: vector<int>::it

HttpURLConnection

(轉)詳解HttpURLConnection 請求響應流程 設定連線引數的方法     setAllowUserInteraction setDoInput setDoOutput setIfModifiedSince se

SpringMVC學習 Dispatcher

前端控制器 Dispatcherservlet 截獲請求後做了什麼工作呢?DispatcherServlet 又是如何分派請求的呢? 分析DispatcherServlet 原始碼如下: protected void initStrategies(ApplicationContext

自動化監控--zabbix中的Macros巨集

巨集 Zabbix支援許多在多種情況下使用的巨集。巨集是一個變數,由如下特殊語法標識: {MACRO} 根據在上下文中, 巨集解析為一個特殊的值。有效地使用巨集可以節省時間,並使Zabbix變地更加高效。 在一個的典型用途中,巨集可以用於模板中。因此,模板的觸發器可能

自動化監控--zabbix中的template模板

模板概述 模板是可以方便地應用於多個主機的一組實體。而這些實體包括:items(監控項)、triggers(觸發器),graphs(圖形)、applications(應用)、screens (聚合圖形(自Zabbix 2.0起))、low-level discovery rules

C#泛型 C#泛型 C#泛型

  一、前面兩篇文章分別介紹了定義泛型型別、泛型委託、泛型介面以及宣告泛型方法:   詳解C#泛型(一)   詳解C#泛型(二)   首先回顧下如何構建泛型類: public class MyClass<T> { public void MyFunc() {

歸併排序MergeSort和動畫

歸併排序演算法思想:將陣列不斷二分得到子陣列,知道子陣列長度為1(自然是排序好的),對左子陣列和右子陣列分別排序, 子陣列長度為1,2,4...., 子陣列排序使用了一個輔助陣列 def exchange(arr,i,j): temp=arr[i] arr[i]=arr[

快速排序Quicksort(動畫程式碼)

快速排序只一種基於分治策略(divide and conquer)的經典排序演算法,並且是一種原地(in-place)排序,實現原地排序的依據是用兩個陣列下標(start,end)來確定當前待排序子陣列的範圍。 切分(partition)步驟(關鍵): 在對子陣列進行排序時,本質上是在確定子

轉載KMP演算法 原創KMP演算法

轉自https://www.cnblogs.com/yjiyjige/p/3263858.html (原創)詳解KMP演算法 作者:孤~影   KMP演算法應該是每一本《資料結構》書都會講的,算是知名度最高的演算法之一了,但很可惜,我大二那年壓根就沒看懂過~~~ 之後也在很多地方也都經常看

selenium藉助AutoIt識別上傳下載

From: http://www.cnblogs.com/fnng/p/4188162.html AutoIt目前最新是v3版本,這是一個使用類似BASIC指令碼語言的免費軟體,它設計用於Windows GUI(圖形使用者介面)中進行自動化操作。它利用模擬鍵盤按鍵,滑鼠移動和視窗/控制元件的組合來

在React中跨元件分發狀態的三種方法

英文原文連結:https://engineering.hexacta.com/three-approaches-to-distribute-the-state-across-components-in-react-da4db5a389e0 當我問自己第一百次時,我正在研究一個典型的CRUD螢幕:“我應該將