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