本文来自《Programming in Scala》一书
scala学习之隐式转换和隐式参数
1 隐式类型转换
scala编译器在对语言做类型检查时,发现参与运行的表达式类型A不符合类型语义要求,在抛出错误之前,会在当前作用域中查找是否存在A 到B类型的转换,是的转换之后通过类型检查,这种转换就是隐式类型转换:用户无需在代码中明确的调用函数将A转换成B,编译器帮你做了这件事。
比如:
val list1 = List(1,2)
val i = 3
list1 ::: i
上面代码无法通过scala编译器的类型检查,抛出如下错误:
scala> val list1 = List(1,2)
list1: List[Int] = List(1, 2)
scala> val i = 3
i: Int = 3
scala> list1 ::: i
<console>:28: error: value ::: is not a member of Int
list1 ::: i
^
原因在于list1:::i
被转换成list1.:::(i)
的方法调用,这个方法要求参数必须是集合类型,而i是Int型。一种解决办法是像这样list1:::List(i)
在代码中显式的将 i 转换成List。另外一种方式就是像下面这样:
object IntWrapper{
implicit def intToList(i : Int): List[Int] = {
List(i)
}
def main(args : Array[String]) :Unit = {
val l1 = List(1,2);
val l2 = l1 ::: 3;
print(l2)
//输出List(1, 2, 3)
}
}
定义隐式转换方法intToList
(必须使用implicit关键字),接收Int类型参数,转换成List[Int]。 上面代码编译器再碰到l1:::3
时,发现类型不匹配,会在当前作用域寻找一种隐式转换使得 3 转换成l1.:::()
调用能够接收的类型。
还有一种会编译器会尝试将源类型隐式转换成目标类型的地方就是:在源类型上调用方法,可是该方法却不存在于源类型的class定义中,比如:
class IntWrapper(i : Int) {
private[IntWrapper] var l = List[Int](i)
def printSelf(): Unit ={
println("In IntWraper: " + l)
}
}
object IntWrapper{
implicit def intToIntWraper(i : Int):IntWrapper = {
new IntWrapper(i)
}
def main(args : Array[String]) :Unit = {
val i = 11;
i.printSelf()
//输出 In IntWraper: List(11)
}
}
main方法里定义了Int类型 i ,Int类型不存在方法printSelf,此时编译器会尝试寻找Int上的类型转换,使的转换后的目标的类型有方法printSelf,因此会使用implicit def intToIntWraper
作为转换函数。
上面这两种情形实际上可以归为一类,就是都需要将源类型转换成目标类型才能够通过类型检查。
2 隐式参数
scala编译器另一个会替你完成隐式操作的地方就是参数列表,比如定义了下面这样一个方法:
class IntWrapper (i : Int){
...
private[IntWrapper] var l = List(i)
def map(implicit m : Int => List[Int]):List[List[Int]] = {
l.map(m)
}
...
}
object IntWrapper{
implicit val f =(i : Int) => List(i + 1)
def main(args : Array[String]) :Unit = {
val intWraper = new IntWrapper(1)
//map使用隐式参数,编译器在当前可见作用域中查找到 f 作为map的参数
print(intWraper.map)
}
}
方法map的参数列表使用了implicit
关键字,表示你在使用map时,可以不提供参数,让编译器去替你完成在方法调用的作用域中去查找满足参数类型的隐式转换。
上面实例代码中表,使用隐式参数时,不仅要求map的形式参数列表使用implicit,提供的隐式实际参数 f 也需要使用implicit。
3 何时发生隐式操作
其实1,2已经说明了scala何时会尝试使用隐式操作:
-
需要转换成目标类型
比如函数 f 的类型A => B,使用类型为C(假设C不是A的子类)的变量c调用
f(c)
, 编译器就会需要尝试寻找将 C => A的隐式类型转换。 -
在原类型上调用不存在的方法
比如class A存在方法
def fA()…
,在class B的实例b上调用b.fA()
,假设class B没有成员方法fA,此时会尝试 B => A的隐式转换. 隐式参数
4 隐式操作的一些规则或者约束
4.1 作用域规则
隐式转换必须以单一标识符的形式出现在需要转换的地方所处的作用域中,或者与转换的源类型或目标类型关联在一起。分别解释一下:
-
单一标识符是指不能以xxx.f这种形式才能使A转换成B类型
假设有隐式转换
f
使得A转换成B,编译器在插入代码时必须是f(A)
,而不需要在f前面插入包或者类的前缀,比如下面这种就是不行的:package me.eric.learn.`implicit`.convertutils object ConvertUtils{ //Int 到 List的隐式转换 implicit def intToList(i : Int): List[Int] = { List(i) } } ------------------------------------------------------------------- package me.eric.learn.`implicit` //引入ConvertUtils import convertutils.ConvertUtils class IntWrapper{} object IntWrapper{ def main(args : Array[String]) :Unit = { val l1 = List(1,2) //此处需要将 3 隐式转换成List,编译器无法直接应用 l1:::intToList(3),当在代码中显式调用时也得这样: l1:::ConvertUtils.intToList(3)。不符合单一标识符的原则 val l2 = l1 ::: 3 } } //上面代码要想可以应用隐式转换,可以使用“import convertutils.ConvertUtils.intToList“引入
-
与转换的源类型关联
和后面介绍的与转换的目标类型关联一样,这是单一标志符原则的例外。原类型关联是指:假设需要A - > B类型转换,那么转换操作可以以
implicit def ...
这种形式定义在object A
里,如下:package me.eric.learn.`implicit` //StringWrapper是源类型,包装一下String class StringWrapper(s : String) { val s1 = s } object StringWrapper{ //隐式操作和源类型关联,StringWrapper -> String转换 implicit def stringWrapperToString(sw : StringWrapper):String = { sw.s1 } //隐式操作, String -> StringWrapper的转换 implicit def stringToStringWrapper(s : String):StringWrapper={ new StringWrapper(s) } } --------------------------------------------------------------------------- package me.eric.learn.`implicit`.convertutils //这种引入方式不符合单一标识符原则 import me.eric.learn.`implicit`.StringWrapper object StringWrapperUtils { def split(sw : StringWrapper):String[]={ //StringWrapper关联的stringWrapperToString,sw可以隐式转换成String,调用split方法 sw.split(" ") } }
-
与目标类型关联
即A -> B的转换可以定义在object B中,以2中代码为例: StringWrapperUtils#split接收参数类型为StringWrapper, 此时传一个String类型是非法的,显然无法在Object String里增加String -> StringWrapper的转换,但是可以在StringWrapper里增加,如同例子中
implicit def stringToStringWrapper...
那样。
4.2 无歧义规则
无歧义规则是指不可以有两个隐式转换 convert1 和convert2 使得转换应用在A上都可以完成类型检查。比如下面的代码就通不过编译;
object IntWrapper{
implicit def intToList(i : Int): List[Int] = {
List(i)
}
implicit def intToList1(i : Int): List[Int] = {
List(i + 1)
}
// implicit val f =(i : Int) => List(i + 2)
implicit def intToIntWraper(i : Int):IntWrapper = {
new IntWrapper(i)
}
def main(args : Array[String]) :Unit = {
val l1 = List(1,2)
val l2 = l1 ::: 3
print(l2)
}
}
忽略上面代码注释部分,有两个Int到List的转换:intToList和intToList1,存在歧义。
但是奇怪的是,去掉上面代码的注释,却又能通过编译,且运行结果显示使用了implicit val f =(i : Int) => List(i + 2)
这个隐式转换。不知道为什么。
4.3 单一调用规则
如果需要对类型A转换,只会进行一次转换,比如方法f(c : C)
接收C类型,此时存在 A -> B
的转换fAB,B -> C
的转换fBC,f(a)
的调用不会成功,不会编译器不会应用f(fBC(fAB(a)))
把a转换成C类型
4.4 显式操作优先
其实很简单,能不进行隐式转换就能通过类型检查的话,就不使用隐式转换。比如方法def f(a : A)
,变量b(类型为B,继承A),调用f(b)
,即便此时有B -> A的转换也不会应用。
5. 隐式参数
4中的规则同样适用于隐式参数,隐式参数适用方式如下:
package me.eric.learn.`implicit`
class ImplicitParam {
// implicit 声明隐式参数
def echo(i : Int)(implicit s:String, l:Long) ={
println((i,s,l))
}
}
object ImplicitParam{
// 作为隐式值应用于ImplicitParam#echo方法
implicit val s = "hello"
implicit val l = 100L
def main(args:Array[String]): Unit ={
val ip = new ImplicitParam()
//只需要指定参数 i的值,编译器将应用 s,l作为隐式参数的实参,相当于调用ip.echo(i)(s,l)
ip.echo(1)
}
}
对于柯里化函数,隐式参数只能出现最后一组参数列表上,只能是形如
def f(...)(...)(implicit ...)
这样.-
隐式值不仅可以用于补足隐式参数,这个隐式值后续还能作为一种可行的隐式转换作用于方法体中的隐式类型转换,比如下面:
package me.eric.learn.`implicit` class ImplicitParam { //需要 Int => List[Int]类型的隐式参数 def echo(implicit intToList : Int => List[Int]) = { val list = List(1,2) val i = 3 //此处需要隐式转换,将i转换成合适的目标类型,此处 intToList将作为一种可行的隐式转换作用于i上 println(i ::: list) } } object ImplicitParam{ //定义 Int => List[Int]的隐式值 implicit val f = (i : Int) => List(i) def main(args:Array[String]): Unit ={ val ip = new ImplicitParam() // f 作为隐式值被应用 ip.echo } }