一、隐式转换
1.1 使用隐式转换
隐式转换指的是以implicit
关键字声明带有单个参数的转换函数,它将值从一种类型转换为另一种类型,以便使用之前类型所没有的功能。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Person(val name: String)
class Thor(val name: String) { def hammer(): Unit = { println(name + "举起雷神之锤") } }
object Thor extends App { implicit def person2Thor(p: Person): Thor = new Thor(p.name) new Person("普通人").hammer() }
输出: 普通人举起雷神之锤
|
1.2 隐式转换规则
并不是你使用implicit
转换后,隐式转换就一定会发生,比如上面如果不调用hammer()
方法的时候,普通人就还是普通人。通常程序会在以下情况下尝试执行隐式转换:
- 当对象访问一个不存在的成员时,即调用的方法不存在或者访问的成员变量不存在;
- 当对象调用某个方法,该方法存在,但是方法的声明参数与传入参数不匹配时。
而在以下三种情况下编译器不会尝试执行隐式转换:
- 如果代码能够在不使用隐式转换的前提下通过编译,则不会使用隐式转换;
- 编译器不会尝试同时执行多个转换,比如
convert1(convert2(a))*b
;
- 转换存在二义性,也不会发生转换。
这里首先解释一下二义性,上面的代码进行如下修改,由于两个隐式转换都是生效的,所以就存在了二义性:
1 2 3 4 5
| implicit def person2Thor(p: Person): Thor = new Thor(p.name) implicit def person2Thor2(p: Person): Thor = new Thor(p.name)
new Person("普通人").hammer()
|
其次再解释一下多个转换的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| class ClassA { override def toString = "This is Class A" }
class ClassB { override def toString = "This is Class B" def printB(b: ClassB): Unit = println(b) }
class ClassC
class ClassD
object ImplicitTest extends App { implicit def A2B(a: ClassA): ClassB = { println("A2B") new ClassB }
implicit def C2B(c: ClassC): ClassB = { println("C2B") new ClassB }
implicit def D2C(d: ClassD): ClassC = { println("D2C") new ClassC }
new ClassD().printB(new ClassA)
new ClassC().printB(new ClassA) }
C2B A2B This is Class B
|
1.3 引入隐式转换
隐式转换的可以定义在以下三个地方:
- 定义在原类型的伴生对象中;
- 直接定义在执行代码的上下文作用域中;
- 统一定义在一个文件中,在使用时候导入。
上面我们使用的方法相当于直接定义在执行代码的作用域中,下面分别给出其他两种定义的代码示例:
定义在原类型的伴生对象中:
1 2 3 4 5
| class Person(val name: String)
object Person{ implicit def person2Thor(p: Person): Thor = new Thor(p.name) }
|
1 2 3 4 5
| class Thor(val name: String) { def hammer(): Unit = { println(name + "举起雷神之锤") } }
|
1 2 3 4
| object ScalaApp extends App { new Person("普通人").hammer() }
|
定义在一个公共的对象中:
1 2 3
| object Convert { implicit def person2Thor(p: Person): Thor = new Thor(p.name) }
|
1 2 3 4 5 6
| import com.myhhub.Convert._
object ScalaApp extends App { new Person("普通人").hammer() }
|
注:Scala自身的隐式转换函数大部分定义在Predef.scala
中,你可以打开源文件查看,也可以在Scala交互式命令行中采用:implicit -v
查看全部隐式转换函数。
二、隐式参数
2.1 使用隐式参数
在定义函数或方法时可以使用标记为implicit
的参数,这种情况下,编译器将会查找默认值,提供给函数调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Delimiters(val left: String, val right: String)
object ScalaApp extends App { def formatted(context: String)(implicit deli: Delimiters): Unit = { println(deli.left + context + deli.right) } implicit val bracket = new Delimiters("(", ")") formatted("this is context") }
|
关于隐式参数,有两点需要注意:
1.我们上面定义formatted
函数的时候使用了柯里化,如果你不使用柯里化表达式,按照通常习惯只有下面两种写法:
1 2 3 4 5 6 7 8
| def formatted(implicit context: String, deli: Delimiters): Unit = { println(deli.left + context + deli.right) }
def formatted( context: String, implicit deli: Delimiters): Unit = { println(deli.left + context + deli.right) }
|
上面第一种写法编译的时候会出现下面所示error
信息,从中也可以看出implicit
是作用于参数列表中每个参数的,这显然不是我们想要到达的效果,所以上面的写法采用了柯里化。
1 2
| not enough arguments for method formatted: (implicit context: String, implicit deli: com.myhhub.Delimiters)
|
2.第二个问题和隐式函数一样,隐式默认值不能存在二义性,否则无法通过编译,示例如下:
1 2 3
| implicit val bracket = new Delimiters("(", ")") implicit val brace = new Delimiters("{", "}") formatted("this is context")
|
上面代码无法通过编译,出现错误提示ambiguous implicit values
,即隐式值存在冲突。
2.2 引入隐式参数
引入隐式参数和引入隐式转换函数方法是一样的,有以下三种方式:
- 定义在隐式参数对应类的伴生对象中;
- 直接定义在执行代码的上下文作用域中;
- 统一定义在一个文件中,在使用时候导入。
我们上面示例程序相当于直接定义执行代码的上下文作用域中,下面给出其他两种方式的示例:
定义在隐式参数对应类的伴生对象中;
1 2 3 4 5
| class Delimiters(val left: String, val right: String)
object Delimiters { implicit val bracket = new Delimiters("(", ")") }
|
1 2 3 4 5 6 7 8
| object ScalaApp extends App {
def formatted(context: String)(implicit deli: Delimiters): Unit = { println(deli.left + context + deli.right) } formatted("this is context") }
|
统一定义在一个文件中,在使用时候导入:
1 2 3
| object Convert { implicit val bracket = new Delimiters("(", ")") }
|
1 2 3 4 5 6 7 8 9
| import com.myhhub.Convert.bracket
object ScalaApp extends App { def formatted(context: String)(implicit deli: Delimiters): Unit = { println(deli.left + context + deli.right) } formatted("this is context") }
|
2.3 利用隐式参数进行隐式转换
1
| def smaller[T] (a: T, b: T) = if (a < b) a else b
|
在Scala中如果定义了一个如上所示的比较对象大小的泛型方法,你会发现无法通过编译。对于对象之间进行大小比较,Scala和Java一样,都要求被比较的对象需要实现java.lang.Comparable接口。在Scala中,直接继承Java中Comparable接口的是特质Ordered,它在继承compareTo方法的基础上,额外定义了关系符方法,源码如下:
1 2 3 4 5 6 7 8
| trait Ordered[A] extends Any with java.lang.Comparable[A] { def compare(that: A): Int def < (that: A): Boolean = (this compare that) < 0 def > (that: A): Boolean = (this compare that) > 0 def <= (that: A): Boolean = (this compare that) <= 0 def >= (that: A): Boolean = (this compare that) >= 0 def compareTo(that: A): Int = compare(that) }
|
所以要想在泛型中解决这个问题,有两种方法:
1. 使用视图界定
1 2 3 4 5 6 7
| object Pair extends App {
def smaller[T<% Ordered[T]](a: T, b: T) = if (a < b) a else b println(smaller(1,2)) }
|
视图限定限制了T可以通过隐式转换Ordered[T]
,即对象一定可以进行大小比较。在上面的代码中smaller(1,2)
中参数1
和2
实际上是通过定义在Predef
中的隐式转换方法intWrapper
转换为RichInt
。
1 2
| @inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)
|
为什么要这么麻烦执行隐式转换,原因是Scala中的Int类型并不能直接进行比较,因为其没有实现Ordered
特质,真正实现Ordered
特质的是RichInt
。
2. 利用隐式参数进行隐式转换
Scala2.11+后,视图界定被标识为废弃,官方推荐使用类型限定来解决上面的问题,本质上就是使用隐式参数进行隐式转换。
1 2 3 4 5 6 7
| object Pair extends App {
def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b
println(smaller(1,2)) }
|
参考资料
- Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1
- 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7