Scala 隐式转换implicit详解

最近在使用Spark-GraphX做图相关的算法分析,想着深入看看源码的实现。由于对Scala不熟悉,只能边看源码边去了解Scala的特性。在分析过程中,发现很多地方使用implicit关键词,就想深入了解下implicit

Implicit简介

implicit,即隐式转换,是支撑Scala易用、容错以及灵活语法的基础。

Scala的隐式转换系统定义了一套良好的查找机制,当代码出现类型编译错误时,编译器试图去寻找一个隐式implicit的转换方法,转换出正确的类型,从而使得编译器能够自我修复,完成编译。

比如,在Spark源码中,经常会发现RDD这个类没有reduceByKeygroupByKey等方法定义,但是却可以在RDD上调用这些方法。这就是Scala隐式转换导致的。

参考Spark源码core/src/main/scala/org/apache/spark/rdd/RDD.scala文件,在RDD这个对象上定义了一个rddToPairRDDFunctions隐式转换(在1.3版本之前,该隐式转换定义在SparkContext对象中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Defines implicit functions that provide extra functionalities on RDDs of specific types.
*
* For example, [[RDD.rddToPairRDDFunctions]] converts an RDD into a [[PairRDDFunctions]] for
* key-value-pair RDDs, and enabling extra functionalities such as `PairRDDFunctions.reduceByKey`.
*/
object RDD {

private[spark] val CHECKPOINT_ALL_MARKED_ANCESTORS =
"spark.checkpoint.checkpointAllMarkedAncestors"

// The following implicit functions were in SparkContext before 1.3 and users had to
// `import SparkContext._` to enable them. Now we move them here to make the compiler find
// them automatically. However, we still keep the old functions in SparkContext for backward
// compatibility and forward to the following functions directly.

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)
}

rddToPairRDDFunctions隐式转换,即将RDD[(K, V)]类型的rdd转换为PairRDDFunctions对象,从而可以在原始的rdd对象上调用reduceByKey之类的方法。

那为什么执行完reductByKey方法后,返回的还是原始的rdd对象了?那是因为每次执行完PairRDDFunctions对象方法后,会返回rdd对象。

类型隐式转换是在需要的时候才会触发,如果调用需要进行隐式转换的方法,隐式转换才会进行,否则还是传统的RDD类型的对象。

隐式转换对于代码重构提供了一种新的思路,比如公司有一个历时悠久的用户认证系统,各大业务系统一般会引入以下认证服务接口进行认证授权:

1
2
3
4
5
class HttpAuthParam {}

trait IAuthService {
def auth(p: HttpAuthParam)
}

但是随着业务的发展,有些需要支持TCP协议的授权认证。一种方法是在IAuthService接口对auth方法进行重载,以满足TCP参数形式,那么就有:

1
2
3
4
5
6
trait IAuthService {
def auth(p: HttpAuthParam)

// 新版中支持对tcp消息鉴权的业务方法
def auth(p: TcpAuthMsg)
}

这种做法的问题在于,一旦业务发生变化,出现了新的参数,势必要修改IAuthService接口,添加新的接口方法,导致接口不稳定。

我们可以定义一个通用稳定的认证授权接口:

1
2
3
4
5
6
7
8
9
10
11
12
class AuthParam {}
trait IAuthService {
// 稳定的授权接口
def auth(p: AuthParam)
}

// 默认接口服务实现
class AuthService extends IAuthService {
def auth(p: AuthParam) = {
println("默认鉴权服务请求")
}
}

可以对AuthService类进行隐式转换,增加TCP服务鉴权方式:

1
2
3
4
5
6
7
8
9
class TcpAuthMsg {}
object tcpAuthService {
// 隐式类转换
implicit class TcpAuth(authService: AuthService) {
def auth(p: TcpAuthMsg) = {
println("TCP鉴权服务请求")
}
}
}

那么就可以在需要调用TCP方式时导入该隐式转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
object Test extends App{

// 导入隐式转换
import tcpAuthService._

// 使用Tcp授权认证
val p = new TcpAuthMsg()
val authService = new AuthService()
authService.auth(p)
}

// 运行结果:
// > scala auth_implicit.scala
// TCP鉴权服务请求

通过这个例子可知,通过类的隐式转换可以在不修改原有系统代码之下,对类进行方法重载,提升接口的稳定性。

当然,万事都是一把双刃剑,implicit使得Scala变的非常灵活,但增加了开发者阅读代码的难度,因为你不知道作者在哪些地方实现了implicit

下面,就让我们来详细看看Scala里支持哪些implicit类型,以及应用隐式转换规则的限定条件。

隐式转换类型

Scala隐式转换类型主要包括以下几种类型:隐式参数、隐式试图、隐式类。

隐式参数

隐式参数是在编译器找不到函数需要某种类型的参数时的一种修复机制。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
object Test{

def foo(amout: Float)(implicit rate: Float): Unit = {
println(amout * rate)
}

def main(args: Array[String]): Unit = {
// 隐式参数
implicit val r = 0.13F // 定义隐式变量
foo(10) // 输出1.3
}
}

方法foo参数中申明了隐式参数rate(即用implicit标记),在调用foo方法时,Scala编译器会从当前作用域中寻找一个相同类型的隐式变量,作为调用参数。如果没有找到找到,则会报错。

隐式视图

隐式视图,是指把一种类型自动转换到另外一种类型,以符合表达式的要求。

隐式视图定义一般用如下形式:implicit def <ConversionName> (<argumentName>: OriginalType): ViewType。在需要的时候,如果隐式作用域里存在这个定义,它会隐式地把 OriginalType 类型的值转换为ViewType 类型的值。

隐式视图包含两种转换类型:隐式类型转换以及隐式方法调用。

隐式类型转换

隐式类型转换是编译器发现传递的数据类型与申明不一致时,编译器在当前作用域查找类型转换方法,对数据类型进行转换。

举个例子:

1
2
3
4
5
6
7
8
9
10
object Test{

def main(args: Array[String]): Unit = {

// 隐式类型转换
implicit def double2Int(d: Double) = d.toInt
var i : Int = 3.5
println(i)
}
}

变量i申明为Int类型,但是赋值Double类型数据,显然编译通不过。这个时候可以借助隐式类型转换,定义DoubleInt规则,编译器就会自动查找该隐式转换,将3.5转换成3,从而达到编译器自动修复效果。

隐式方法调用

隐式方法调用是当编译器发现一个对象存在未定义的方法调用时,就会在当前作用域中查找是否存在对该对象的类型隐式转换,如果有,就查找转换后的对象是否存在该方法,存在,则调用。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
object Test {
class Horse {
def drinking(): Unit = {
println("I can drinking")
}
}

class Crow {}

object drinking{
// 隐式方法调用
implicit def extendSkill(c: Crow) = new Horse()
}

def main(args: Array[String]): Unit = {
// 隐式转换调用类中不存在的方法
import drinking._
var crow = new Crow()
crow.drinking()
}
}

crow对象并没有drinkging()方法定义,但是通过隐式规则转换,可以扩展crow对象功能,使其可以拥有Horse对象的功能。

隐式类

Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Test {
class Crow {}

object crow_eval{
implicit class Parrot(animal: Crow) {
def say(say: String): Unit = {println(s"I have the skill of Parrot: $say")}
}
}

def main(args: Array[String]): Unit = {
// 隐式类
import crow_eval._
crow.say("balabala")
}
}

创建隐式类时,只需要在对应的类前加上implicit关键字。

使用隐式类时,需要注意以下几点:

  • 构造参数有且只有一个,且为非隐式参数
  • 隐式类必须被定义在类、伴生对象和包对象里
  • 隐式类不能是case classcase class会自动生成伴生对象,与上一条矛盾)
  • 作用域内不能有与之同名的标识符

隐式解析机制

编译器是如何查找到缺失信息的,解析具有以下两种规则:

  1. 首先会在当前代码作用域下查找隐式实体(隐式方法、隐式类、隐式对象)

  2. 如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找。

    类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型T,它的查找范围如下:

    • 如果T被定义为T with A with B with C,那么A、B、C都是T的部分,在T的隐式解析过程中,它们的伴生对象都会被搜索
    • 如果T是参数化类型,那么类型参数和与类型参数相关联的部分都算作T的部分,比如List[String]的隐式搜索会搜索List的伴生对象和String的伴生对象
    • 如果T是一个单例类型p.T,即T是属于某个p对象内,那么这个p对象也会被搜索
    • 如果T是个类型注入S#T,那么S和T都会被搜索

总结

implicit是Scala语言中处理编译类型错误的一种修复机制,利用该机制,可以编写出任意参数和返回值的多态方法(Ad-hoc polymorphism任意多态),实现任意多态。

隐式转换一般需要具备以下前提条件:

  • 不存在二义性
  • 隐式操作不能嵌套使用,比如需要C类型参数,而实际类型为A,作用域内存在A => B,B => C的隐式方法,Scala编译器不会尝试先调用A => B ,再调用B => C
  • 代码能够在不使用隐式转换的前提下能编译通过,就不会进行进行隐式转换

在实际开发过程中,如果对隐式规则掌握不清楚,往往出了问题会无从查起,笔者建议谨慎使用。

参考文献

  1. Scala中的Implicit详解
  2. https://docs.scala-lang.org/zh-cn/overviews/core/implicit-classes.html
  3. scala implicit 隐式转换
感谢你对我的支持,让我继续努力分享有用的技术和知识点