Scala 专题教程-隐式变换和隐式参数(2):使用implicits的一些规则

jerry Scala 2015年11月25日 收藏

在Scala中的implicit定义指编译器在需要修复类型匹配时可以用来自动插入的定义。比如说,如果x+y类型不匹配,那么编译器可能试着使用convert(x) + y, 其中convert由某个implicit定义的,这有点类似一个整数和一个浮点数相加,编译器可以自动把整数转换为浮点数。Scala的implicit定义是对这种情况的一个推广,你可以定义一个类型在需要时,如何自动转换成另外一种类型。
Scala的implicit定义符合下面一些规则:
标记规则
只有哪些使用implicit关键字的定义才是可以使用的隐式定义。关键字implicit用来标记一个隐式定义。编译器才可以选择它作为隐式变化的候选项。你可以使用implicit来标记任意变量,函数或是对象。
例如下面为一个隐式函数定义:

implicit def intToString(x:Int) : x.toString

编译器只有在convert被标记成implicit 才会将x + y 改成convert(x) + y .当然这是在 x + y类型不匹配时。

范围规则
编译器在选择备选implicit定义时,只会选取当前作用域的定义,比如说编译器不会去调用someVariable.convert。如果你需要使用someVariable.convert,你必须把someVarible引入到当前作用域。也就是说编译器在选择备选implicit时,只有当convert是当前作用域下单个标志符时才会作为备选implicit。比如说,对于一个函数库来说,在一个Preamble对象中定义一些常用的隐式类型转换非常常见,因此需要使用Preamble的代码可以使用?import Preamble._? 把这些implicit定义引入到当前作用域才可以。
这个规则有一个例外,编译器也会在类的伙伴对象定义中查找所需的implicit定义。例如下面的定义:

object Dollar {
	implicit def dollarToEuro(x:Dollar):Euro = ...
	...
}

class Dollar {
   ...
}

如果在class Dollar的方法有需要Euro类型,但输入数据使用的是Dollar,编译器会在其伙伴对象object Dollar查找所需的隐式类型转换,定义一个从Dollar到Euro的implicit 定义可以使用。

一次规则
编译器在需要使用implicit定义时,只会试图转换一次,也就是编译器永远不会把x + y改写成 convert1(convert2(x)) + y。

优先规则
编译器不会在 x+y 已经是合法的情况下去调用implicit 规则。

命名规则
你可以为implicit定义任意的名称。通常情况下你可以任意命名,implicit的名称只在两种情况下有用:一是你想在一个方法中明确指明,另外一个是想把那一个引入到当前作用域。比如我们定义一个对象,包含两个implicit定义:

object MyConversions {
	implicit def stringWrapper(s:String):IndexedSeq[Char] = ...
	implicit def intToString(x:Int):String = ...
}

在你的应用中,你想使用stringWrapper变换,而不想把整数自动转换成字符串,你可以只引入stringWrapper。

import  MyConversions.stringWrapper

编译器使用implicit的几种情况
有三种情况使用implicit: 一是转换成预期的数据类型,而是转换selection的receiver,三是隐含参数。
转换成预期的数据类型比如你有一个方法参数类型是IndexedSeq[Char],在你传入String时,编译器发现类型不匹配,就检查当前作用域是否有从String到IndexedSeq隐式转换。
转换selection的receiver允许你适应某些方法调用,比如 ?abc?.exist ,?abc?类型为String,本身没有定义exist方法,这时编辑器就检查当前作用域内String的隐式转换后的类型是否有exist方法,发现stringWrapper转换后成IndexedSeq类型后,可以有exist方法,这个和C# 静态扩展方法功能类似。
隐含参数有点类似是缺省参数,如果在调用方法时没有提供某个参数,编译器会查找当前作用域是否有符合条件的implicit对象作为参数传入(有点类似dependency injection)