由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

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