由sqlContext.implicits._帶來的一場血案 原

先來看看Spark裏關於implicit的實現,如下:

object implicits extends SQLImplicits with Serializable {
  protected override def _sqlContext: SQLContext = self

  implicit class StringToColumn(val sc: StringContext) {
    def $(args: Any*): ColumnName = {
      new ColumnName(sc.s(args: _*))
    }
  }
}

其中 SQLImplicits 是一個抽象類,包含了一系列的隱式轉換,不過主要負責將scala對象轉換爲DataFrame。我們主要用到的是$符,涉及到的是StringToColumn這個隱式類裏的$方法,所以重點看這個方法。

一開始以爲它只是個普通的隱式類,我們知道隱式類一般來說這麼用

object Implicits {
  implicit class RangeMaker(val start: Int) {
    def -->(end: Int) = start to end
  }
}
// 接下來調用如下
import Implicits._
1 --> 10 mkString(",")  => 1,2,3,4,5,6,7,8,9,10

這裏隱式類RangeMaker相當於給Int類型添加了一個-->方法,所以1 --> 10實際是new RangeMaker(1).-->(10)的調用。 但是我們看Spark裏的隱式方法調用,它是df.select($"columnA", $"columnB"),我們發現$"xx"沒法轉成new StringToColumn(xx).$(xx)調用方式,那它是怎麼工作的呢? 我們照它仿寫一個例子:

object Implicits { implicit class Test(val a: Int) { def $(b: Int) = b }}
// 調用測試
import Implicits._
$12 => error: not found: value $12,   $(12) => error: not found: value $

我們會發現,這種調用方式是不成功的,因爲它根本是錯的。所以我又懷疑是Spark相應類裏有上下文,結果找了很久也沒什麼發現。所以,說明特殊性必是與StringContext這個類有關係,查了下發現它是個字符串插值類,進去看了才發現問題果然出在這。

它是case class StringContext(parts: String*) { ... },提供了s, f, raw三種字符串插值方式,我們常用的是s"xxx$xx",這裏會將變量xx進行替換。而f則可以進行字符串的簡單格式化,如f"$name%s is $height%2.2f"raw插值器則跟s插值器差不多,不過它不會轉\n

而對於s"xxx$xx"它的調用方式實際是new StringContext("xxx").s(xx),這個我們在類裏面並沒有找到,因爲它是scala編譯器做了特殊處理的,StringContext類是由內核負責處理的特殊類,它只放出來了部分插接點來讓我們做擴展。可參見SIP文檔

Spark裏的這個隱式類,實際是給StringContext類擴展了一個$方法,所以當我們調用$"columnA"的時候,實際是new StringToColumn(new StringContext("columnA")).$(),這些都是編譯器做的處理!

來個小測試,自己來擴展StringContext的方法,代碼如下:

object Implicits { implicit class Test(val sc: StringContext) extends AnyVal { def back(args: Any*) = args }}
// 調用
import Implicits._
back"123"  => List()
val name = 11; back"name: $name"  => WrappedArray(11)
val name = 11; val id = 12; back"name: $name, id: $id"  => WrappedArray(11, 12)

這裏back方法是把傳入的參數原樣輸出,back"123"實際的調用是new Test(new StringContext("123")).back(),所以輸出爲空,與Spark裏常用的$"columnA"一致。val name = 11; back"name: $name"實際的調用是new Test(new StringContext("name: ")).back(11),所以輸出的是WrappedArray(11)。對於val name = 11; val id = 12; back"name: $name, id: $id"實際調用的是new Test(new StringContext("name: ", ", id: ")).back(11, 12),所以輸出的是WrappedArray(11, 12)

由此可以看出,對於back"name: $name, id: $id"StringContext類的方法邏輯是先拆分裏面的變量,用"name: ", ", id: "來初始化StringContext類,賦給parts成員變量,然後再把參數裏的變量替換爲實際值賦給方法。

歡迎轉載,但請註明出處:https://my.oschina.net/u/2539801/blog/776027

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章