1. 隐式转换简介
在scala语言当中,隐式转换是一项强大的程序语言功能,它不仅能够简化程序设计,也能够使程序具有很强的灵活性。
在scala语言中,隐式转换是无处不在的,只不过scala语言为我们隐藏了相应的细节,例如scala中的类继承层次结构中:
它们存在固有的隐式转换,不需要人工进行干预,例如Float在必要情况下自动转换为Double类型
在前面 关于 Scala 界定、隐式转换的一些知识(三)——视图界定 中我们也提到,视图界定可以跨越类层次结构进行,它背后的实现原理就是隐式转换,例如 Int 类型会视图界定中会自动转换成 RichInt, 而 RichInt 实现了 Comparable 接口,当然这里面的隐式转换也是 scala 语言为我们设计好的
本节将对隐式转换中的隐式转换函数、隐式转换规则、隐式参数进行介绍,使大家明白如何自己实现隐式转换操作。
2. 隐式转换函数
我们在介绍 视图界定的时候,提到 视图界定 的实现 用到了隐式转换,并用 Int 隐式转换成 RichInt 来举例。而下面我们将再举一个例子。
下列赋值如果没有隐式转换的话会报错:
val x:Int = 3.5
添加隐式转换函数后可以实现 Double 类型到 Int 类型的赋值:
implicit def double2Int(x:Double) = x.toInt
val x:Int = 3.5
隐式转换功能十分强大,可以快速地扩展现有类库的功能,例如下面的代码:
package cn.zyb
import java.io.File
import scala.io.Source
class RichFile(val file: File) {
def read = Source.fromFile(file).getLines().mkString
}
object ImplicitFunction extends App {
implicit def double2Int(x: Double) = x.toInt
var x: Int = 3.5
//隐式函数将java.io.File隐式转换为 RichFile 类
implicit def file2RichFile(file: File) = new RichFile(file)
val f = new File("file.log").read
println(f)
}
3. 隐式转换规则
我们下面会介绍什么时候会发生隐式转换,什么时候不会发生。
3.1 何时触发隐式转换
什么时候会发生隐式转换呢?主要有以下几种情况:
3.1.1 当方法中参数的类型与实际类型不一致时
def f(x: Int) = x + 1
//方法中输入的参数类型与实际类型不一致,会自动寻找 隐式转换函数 double2Int(x: Double)
//如果 double2Int(x: Double) 的参数,和 f(x: Int) 实际输入参数类型一致
//且 double2Int 转换后的返回类型 和 f(x: Int) 函数定义的参数类型一致,则满足匹配
//double 类型会自动调用 此隐式函数,转换为 Int 类型,再进行方法的执行
f(3.14)
3.1.2 当调用类中不存在的方法或成员时
例如上面 RichFile 的代码:
//File类的对象并不存在 read 方法,此时便会发生隐式转换
//将File类转换成 RichFile
val f = new File("file.log").read
3.2 何时不会触发隐式转换
3.2.1 编译器可以正常编译通过时
//下面几条语句,不需要自己定义隐式转换编译就可以通过
//因此它不会发生前面定义的隐式转换
scala> 3.0*2
res0: Double = 6.0
scala> 2*3.0
res1: Double = 6.0
scala> 2*3.7
res2: Double = 7.4
3.2.2 转换存在二义性
//这里定义了一个隐式转换
implicit def file2RichFile(file:File)=new RichFile(file)
//这里又定义了一个隐式转换,目的与前面那个相同
implicit def file2RichFile2(file:File)=new RichFile(file)
//下面这条语句在编译时会出错,报错信息如下(去除了无用信息)
//both method file2RichFile and method file2RichFile2
//are possible conversion functions from java.io.File to ?{def read: ?}
val f=new File("file.log").read
3.2.3 隐式转换不会嵌套进行
源类型到目标类型的转换只会进行一次
下面看一下不完全代码:
implicit def richFile2RichFileAnother(richFile: RichFile) = new RichFileAnother(richFile)
//RichFileAnother类,里面定义了read2方法
class RichFileAnother(val richFile: RichFile) {
def read2 = file.read
}
//隐式转换不会多次进行,下面的语句会报错
//不能期望会发生 File 到 RichFile,然后 RifchFile 到 RichFileAnthoer 的转换
val f = new File("file.log").read2
4. 隐式参数
我们在 关于 Scala 界定、隐式转换的一些知识(五)——上下文界定 中,说到 上下文界定用到了 隐式参数。如果给函数定义隐式参数的话,则在使用时可以不带参数
class Pair[T: Ordering](val first: T, val second: T) {
//smaller 方法中有一个隐式参数,该隐式参数类型为 Ordering[T]
def smaller(implicit ord: Ordering[T]) = {
if(ord.compare(first, second) > 0) first else second
}
}
implicit val p1 = new PersonOrdering
//不给函数指定参数,此时会查找一个隐式值,该隐式值类型为 Ordering[Person]
//根据上下文界定的要求,p1 正好满足要求
//因此它会作为 smaller 的隐式参数传入,从而调用 ord.compare(first,second) 方法进行比较
val p = new Pair(Person("123"), Person("456"))
//调用时并没有传入隐式参数
p.smaller
5. 隐式参数中的隐式转换
前一讲中,我们提到函数中如果存在隐式参数,在使用该函数时 编译器会自动帮我们搜索相应的隐式值,并将该隐式值作为函数的参数
这里面其实没有涉及到隐式转换,本节将演示如何利用隐式参数进行隐式转换,下面的代码给定的是一个普通的比较函数:
object ImplicitParameter extends App {
//下面的代码不能编译通过,这里面泛型T没有具体指定
//它不能直接使用 < 符号进行比较
def compare[T](first: T, second: T) = {
if (first < second) first else second
}
}
面的代码要想使其编译通过,可以为 T 指定其上界为 Ordered[T]
object ImplicitParameter extends App {
def compare[T <: Ordered[T]](first: T, second: T) = {
if (first < second) first else second
}
}
这是一种解决方案,我们还有一种解决方案就是通过隐式参数的隐式转换来实现,代码如下:
object ImplicitParameter extends App {
def compare[T](first: T, second: T)(implicit order: T => Ordered[T]) = {
if (first < second) first else second
}
}
6. 隐式转换问题梳理
6.1 多次隐式转换问题
在 3.2.3 隐式转换不会嵌套进行 中我们提到,源类型到目标类型的转换只会进行一次,并不是说不存在多次隐式转换,在一般的方法调用过程中可能会出现多次隐式转换,例如:
class ClassA {
override def toString() = "This is Class A"
}
class ClassB {
override def toString() = "This is Class B"
}
class ClassC {
override def toString() = "This is ClassC"
def printC(c: ClassC) = println(c)
}
class ClassD
object ImplicitWhole extends App {
implicit def B2C(b: ClassB) = {
println("B2C")
new ClassC
}
implicit def D2C(d: ClassD) = {
println("D2C")
new ClassC
}
//下面的代码会进行两次隐式转换
//因为ClassD中并没有printC方法
//因为它会隐式转换为ClassC(这是第一次,D2C)
//然后调用printC方法
//但是printC方法只接受ClassC类型的参数
//然而传入的参数类型是ClassB
//类型不匹配,从而又发生了一次隐式转地换(这是第二次,B2C)
//从而最终实现了方法的调用
new ClassD().printC(new ClassB)
}
还有一种情况也会发生多次隐式转换,如果给函数定义了隐式参数,在实际执行过程中可能会发生多次隐式转换,代码如下:
object Main extends App {
class PrintOps() {
def print(implicit i: Int) = println(i);
}
implicit def str2PrintOps(s: String) = {
println("str2PrintOps")
new PrintOps
}
implicit def str2int(implicit s: String): Int = {
println("str2int")
Integer.parseInt(s)
}
implicit def getString = {
println("getString")
"123"
}
//下面的代码会发生三次隐式转换
//首先编译器发现String类型是没有print方法的
//尝试隐式转换,利用str2PrintOps方法将String
//转换成PrintOps(第一次)
//然后调用print方法,但print方法接受整型的隐式参数
//此时编译器会搜索隐式值,但程序里面没有给定,此时
//编译器会尝试调用 str2int方法进行隐式转换,但该方法
//又接受一个implicit String类型参数,编译器又会尝试
//查找一个对应的隐式值,此时又没有,因此编译器会尝试调用
//getString方法对应的字符串(这是第二次隐式转换,
//获取一个字符串,从无到有的过程)
//得到该字符串后,再调用str2int方法将String类型字符串
//转换成Int类型(这是第三次隐式转换)
"a".print
}
6.2 要不要用隐式转换的问题
从上述代码中可以看到,隐式转换功能很强大,但同时也带来了程序复杂性性问题,在一个程序中如果大量运用隐式转换,特别是涉及到多次隐式转换时,会使代码理解起来变得比较困难,那到底要不要用隐式转换呢?下面给出我自己开发实践中的部分总结,供大家参考:
- 即使你能轻松驾驭 scala 语言中的隐式转换,能不用隐式转换就尽量不用
- 如果一定要用,在涉及多次隐式转换时,必须要说服自己这样做的合理性
- 如果只是炫耀自己的scala语言能力,请大胆使用