先来看看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