最近在使用Spark-GraphX
做图相关的算法分析,想着深入看看源码的实现。由于对Scala
不熟悉,只能边看源码边去了解Scala
的特性。在分析过程中,发现很多地方使用implicit
关键词,就想深入了解下implicit
。
Implicit简介
implicit,即隐式转换,是支撑Scala
易用、容错以及灵活语法的基础。
Scala的隐式转换系统定义了一套良好的查找机制,当代码出现类型编译错误时,编译器试图去寻找一个隐式
implicit
的转换方法,转换出正确的类型,从而使得编译器能够自我修复,完成编译。
比如,在Spark
源码中,经常会发现RDD
这个类没有reduceByKey
、groupByKey
等方法定义,但是却可以在RDD
上调用这些方法。这就是Scala
隐式转换导致的。
参考Spark
源码core/src/main/scala/org/apache/spark/rdd/RDD.scala
文件,在RDD
这个对象上定义了一个rddToPairRDDFunctions
隐式转换(在1.3版本之前,该隐式转换定义在SparkContext对象中)。
1 | /** |
rddToPairRDDFunctions
隐式转换,即将RDD[(K, V)]
类型的rdd转换为PairRDDFunctions
对象,从而可以在原始的rdd对象上调用reduceByKey
之类的方法。
那为什么执行完reductByKey
方法后,返回的还是原始的rdd
对象了?那是因为每次执行完PairRDDFunctions
对象方法后,会返回rdd
对象。
类型隐式转换是在需要的时候才会触发,如果调用需要进行隐式转换的方法,隐式转换才会进行,否则还是传统的RDD类型的对象。
隐式转换对于代码重构提供了一种新的思路,比如公司有一个历时悠久的用户认证系统,各大业务系统一般会引入以下认证服务接口进行认证授权:
1 | class HttpAuthParam {} |
但是随着业务的发展,有些需要支持TCP
协议的授权认证。一种方法是在IAuthService
接口对auth
方法进行重载,以满足TCP
参数形式,那么就有:
1 | trait IAuthService { |
这种做法的问题在于,一旦业务发生变化,出现了新的参数,势必要修改IAuthService
接口,添加新的接口方法,导致接口不稳定。
我们可以定义一个通用稳定的认证授权接口:
1 | class AuthParam {} |
可以对AuthService
类进行隐式转换,增加TCP
服务鉴权方式:
1 | class TcpAuthMsg {} |
那么就可以在需要调用TCP方式时导入该隐式转换:
1 | object Test extends App{ |
通过这个例子可知,通过类的隐式转换可以在不修改原有系统代码之下,对类进行方法重载,提升接口的稳定性。
当然,万事都是一把双刃剑,implicit
使得Scala变的非常灵活,但增加了开发者阅读代码的难度,因为你不知道作者在哪些地方实现了implicit
。
下面,就让我们来详细看看Scala里支持哪些implicit
类型,以及应用隐式转换规则的限定条件。
隐式转换类型
Scala隐式转换类型主要包括以下几种类型:隐式参数、隐式试图、隐式类。
隐式参数
隐式参数是在编译器找不到函数需要某种类型的参数时的一种修复机制。
举个例子:
1 | object Test{ |
方法foo
参数中申明了隐式参数rate
(即用implicit
标记),在调用foo
方法时,Scala编译器会从当前作用域中寻找一个相同类型的隐式变量,作为调用参数。如果没有找到找到,则会报错。
隐式视图
隐式视图,是指把一种类型自动转换到另外一种类型,以符合表达式的要求。
隐式视图定义一般用如下形式:implicit def <ConversionName> (<argumentName>: OriginalType): ViewType
。在需要的时候,如果隐式作用域里存在这个定义,它会隐式地把 OriginalType
类型的值转换为ViewType
类型的值。
隐式视图包含两种转换类型:隐式类型转换以及隐式方法调用。
隐式类型转换
隐式类型转换是编译器发现传递的数据类型与申明不一致时,编译器在当前作用域查找类型转换方法,对数据类型进行转换。
举个例子:
1 | object Test{ |
变量i
申明为Int
类型,但是赋值Double
类型数据,显然编译通不过。这个时候可以借助隐式类型转换,定义Double
转Int
规则,编译器就会自动查找该隐式转换,将3.5
转换成3
,从而达到编译器自动修复效果。
隐式方法调用
隐式方法调用是当编译器发现一个对象存在未定义的方法调用时,就会在当前作用域中查找是否存在对该对象的类型隐式转换,如果有,就查找转换后的对象是否存在该方法,存在,则调用。
举个例子:
1 | object Test { |
crow
对象并没有drinkging()
方法定义,但是通过隐式规则转换,可以扩展crow
对象功能,使其可以拥有Horse
对象的功能。
隐式类
Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换。
举个例子:
1 | object Test { |
创建隐式类时,只需要在对应的类前加上implicit
关键字。
使用隐式类时,需要注意以下几点:
- 构造参数有且只有一个,且为非隐式参数
- 隐式类必须被定义在类、伴生对象和包对象里
- 隐式类不能是
case class
(case class
会自动生成伴生对象,与上一条矛盾) - 作用域内不能有与之同名的标识符
隐式解析机制
编译器是如何查找到缺失信息的,解析具有以下两种规则:
首先会在当前代码作用域下查找隐式实体(隐式方法、隐式类、隐式对象)
如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找。
类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型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都会被搜索
- 如果T被定义为
总结
implicit
是Scala语言中处理编译类型错误的一种修复机制,利用该机制,可以编写出任意参数和返回值的多态方法(Ad-hoc polymorphism
任意多态),实现任意多态。
隐式转换一般需要具备以下前提条件:
- 不存在二义性
- 隐式操作不能嵌套使用,比如需要
C
类型参数,而实际类型为A
,作用域内存在A => B
,B => C
的隐式方法,Scala编译器不会尝试先调用A => B
,再调用B => C
。 - 代码能够在不使用隐式转换的前提下能编译通过,就不会进行进行隐式转换
在实际开发过程中,如果对隐式规则掌握不清楚,往往出了问题会无从查起,笔者建议谨慎使用。