前言
很久没写过关于Scala的东西了(其实是不敢班门弄斧)。Scala的隐式机制在之前读Spark代码时就令人感觉fascinated,今天稍微聊聊吧。
隐式(implicit)机制是Scala的一个重要而有趣的特性,能够使Scala编程更加灵活和可扩展。我在日常编码中虽然几乎没用过它,但是在阅读某些主要用Scala写成的开源框架时,就可以说是遍地开花了。本文借助Spark中一些源码,来看看隐式机制是如何发挥作用的。
隐式参数
Scala中文官方文档中对隐式参数的解释如下:
方法可以具有隐式参数列表,由参数列表开头的implicit关键字标记。如果参数列表中的参数没有像往常一样传递,Scala将查看它是否可以获得正确类型的隐式值,如果可以,则自动传递。
Scala将查找这些参数的位置分为两类:
- Scala在调用包含有隐式参数块的方法时,将首先查找可以直接访问的隐式定义和隐式参数 (无前缀)。
- 然后,它在所有伴生对象中查找与隐式候选类型相关的有隐式标记的成员。
以Spark为例,RDD的许多算子都会接受一个Ordering[T]类型的隐式参数,它用于指定RDD中元素类型T的排序规则。比如takeOrdered()算子,其代码如下:
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
if (num == 0) {
Array.empty
} else {
val mapRDDs = mapPartitions { items =>
// Priority keeps the largest elements, so let's reverse the ordering.
val queue = new BoundedPriorityQueue[T](num)(ord.reverse)
queue ++= collectionUtils.takeOrdered(items, num)(ord)
Iterator.single(queue)
}
if (mapRDDs.partitions.length == 0) {
Array.empty
} else {
mapRDDs.reduce { (queue1, queue2) =>
queue1 ++= queue2
queue1
}.toArray.sorted(ord)
}
}
}
它从RDD中按隐式参数ord规定的顺序取出前N个元素。在spark-shell中测试一下:
如果不传参数ord的话,默认就会按升序排序,那么默认排序是哪里来的呢?按照官方文档的描述,我们先在当前代码作用域中找隐式Ordering定义(没有),然后在RDD类与伴生对象中找(没有),然后在Ordering与Int中找,最后可以发现Ordering伴生对象中有如下定义:
trait IntOrdering extends Ordering[Int] {
def compare(x: Int, y: Int) =
if (x < y) -1
else if (x == y) 0
else 1
}
implicit object Int extends IntOrdering
可见,编译器查找到并发挥作用的是隐式对象Int,它最终继承自Ordering[Int]特征并实现了compare()方法,其中规定了升序排序的逻辑。Ordering伴生对象中还定义了很多其他类型的隐式排序规则,除了基本类型之外,还包括String、BigDecimal甚至Option等。
如果我们不用那些已经规定好的类型,而换成自定义类型,就会报错。因为并没有自定义类型对应的隐式排序规则。例如下图:
于是我们就自己定义一个Ordering[Temp]类型的隐式值,直接定义或者新建一个Temp类的伴生对象都可以。再调用takeOrdered()算子,就可以得出正确的结果。
由此可见,隐式值、隐式对象(也包括下面的隐式方法和隐式类)的名称并没有什么要紧,重要的是类型本身。用上面的例子来说,如果我们同时定义了两个Ordering[Temp]类型的隐式值,那么编译器就会报错,因为会出现二义性,无法确定该使用哪一个了。
隐式转换
Scala中文官方文档中对隐式转换的解释如下:
一个从类型S到类型T的隐式转换由一个函数类型S => T的隐式值来定义,或者由一个可转换成所需值的隐式方法来定义。
隐式转换在两种情况下会用到:
- 如果一个表达式e的类型为S,并且类型S不符合表达式的期望类型T。
- 在一个类型为S的实例对象e中调用e.m,如果被调用的m并没有在类型S中声明。
在第一种情况下,搜索转换c,它适用于e,并且结果类型为T。在第二种情况下,搜索转换c,它适用于e,其结果包含名为m的成员。
这个解释有点过于文绉绉了,下面仍然通过例子来说明。
我们经常会对RDD(准确来说是Pair RDD,即RDD[(K,V)])使用诸如rdd.aggregateByKey()、rdd.reduceByKey()、rdd.join()这类算子,但它们在RDD抽象类本身并没有定义,在RDD类的所有子类中也同样找不到。这是因为在RDD类的伴生对象中,有如下的隐式转换方法。
implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
(implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = {
new PairRDDFunctions(rdd)
}
它的作用是将RDD[(K,V)]类型隐式转换成PairRDDFunctions类型,而PairRDDFunctions类中定义了这些算子。
可见,这适用于官方文档中说明的第二种情况。假设我们执行rdd.reduceByKey(),由于RDD类没有定义该算子,编译器就会按顺序寻找源类型为RDD[(K,V)]的隐式转换,并最终找到隐式转换方法rddToPairRDDFunctions()中,目标类型PairRDDFunctions定义了reduceByKey()算子,执行成功。
那么第一种情况呢?在SparkContext.scala中有个WritableConverter类,其伴生对象中有很多这样的隐式值定义:
implicit val intWritableConverterFn: () => WritableConverter[Int] =
() => simpleWritableConverter[Int, IntWritable](_.get)
implicit val longWritableConverterFn: () => WritableConverter[Long] =
() => simpleWritableConverter[Long, LongWritable](_.get)
implicit val doubleWritableConverterFn: () => WritableConverter[Double] =
() => simpleWritableConverter[Double, DoubleWritable](_.get)
implicit val floatWritableConverterFn: () => WritableConverter[Float] =
() => simpleWritableConverter[Float, FloatWritable](_.get)
implicit val booleanWritableConverterFn: () => WritableConverter[Boolean] =
() => simpleWritableConverter[Boolean, BooleanWritable](_.get)
它们的作用都是隐式地将Hadoop I/O体系中的Writable数据类型转化为基本数据类型,更贴近于我们平常理解的类型转换了。
同样地,隐式转换也不能存在二义性(不能定义两个源类型和目标类型都相同的转换),并且转换是单向进行的。
隐式类
隐式类是Scala 2.10开始引入的新特性,关于它的说明,可以参考https://docs.scala-lang.org/zh-cn/overviews/core/implicit-classes.html。由于它的历史并不算长,因此Spark中用到的也并不多。在其通用工具类Utils中有一个,名为Lock,如下:
private implicit class Lock(lock: LockInfo) {
def lockString: String = {
lock match {
case monitor: MonitorInfo =>
s"Monitor(${lock.getClassName}@${lock.getIdentityHashCode}})"
case _ =>
s"Lock(${lock.getClassName}@${lock.getIdentityHashCode}})"
}
}
}
这样就相当于给LockInfo类新增了一个方法lockString(),用来以字符串形式打印出锁信息。在Utils类中就有了以下这样的调用:
val heldLocks = (threadInfo.getLockedSynchronizers ++ threadInfo.getLockedMonitors).map(_.lockString).toSet
可见,隐式类可以方便地对现有的类进行扩展。
如何寻找隐式定义
在Scala官网的一篇文章https://docs.scala-lang.org/tutorials/FAQ/finding-implicits.html中,详细讲述了Scala是怎样在代码中寻找隐式定义的,简要总结如下:
- 在当前代码作用域中寻找隐式定义(隐式值/方法/对象/类),如果找得到,即应用之。
- 若在当前代码作用域没有找到,就检查与隐式类型T相关的以下所有范围:
- T的伴生对象;
- T继承或实现的类、特征等的伴生对象;
- 包含T的参数化类型的伴生对象(即如果是S[T]类型,也要检查S的伴生对象);
- T的外部类的伴生对象(仅当T是内部类时)。
The End
民那晚安。祝身体健康,百毒不侵。