1. Background
implicits是Scala的一种预编译特性,
编译器会根据implicit相关的规则,在程序中自动插入代码,
以修正类型错误(type error)。
例如,如果x+y
不能通过类型检查,
则编译器可能会将它转化为convert(x)+y
,
其中,convert
是一些可用的隐式转换(implicit conversion)。
如果convert
可以将x
转换成含有+
方法的对象,
那么这种转换,就可以修复程序中的类型错误。
2. Rules for implicits
2.1 Marking Rule
Scala编译器,只使用那些显式被关键字implicit
注明的函数/对象。
例如,
implicit def intToString(x: Int) = x.toString
2.2 Scope Rule
Scala编译器,只使用那些被导入到当前模块中的函数/对象,
并且,还必须是单独的标识符(single identifier)。
即,编译器不会自动添加这样的转换someVariable.convert
,
除非使用import someVariable.convert
,将它导入为单独的标识符convert
。
注:
除了作用域内的函数/对象之外,
编译器还会检查伴随对象(companion object)中的implicit定义,例如,
object Dollar {
implicit def dollarToEuro(x: Dollar): Euro = ...
}
class Dollar { ... }
其中,dollarToEuro
是不用显式导入的,编译器会自己查找到它。
2.3 Non-Ambiguity Rule
如果为了修复x+y
有两个不同的选择,
例如,convert1(x)+y
和convert2(x)+y
,
那么编译器会直接报错。
为了解决这个问题,一种办法是删除一个来避免歧义,
另一种方法是,将转换方法显式的写出来,例如,convert2(x)+y
。
2.4 One-at-a-time Rule
编译器并不会把x+y
写成convert1(convert2(x))+y
,
在尝试一种转换的过程中,中途并不会再次尝试其他转换。
2.5 Explicits-First Rule
编译器不会对类型良好的程序进行转换。
注:
注明为implicit的函数/对象,可以具有任意的名字。编译器是通过类型来查找合适的implicit函数/对象的,而不是通过名字。
3. Where implicits are tried
implicit会在以下三种情况中出现,
(1)对期望出现的类型进行隐式转换(implicit conversion to an expected type)
(2)对消息的接受者进行转换(converting the receiver)
(3)隐式参数(implicit parameters)
3.1 Implicit conversion to an expected type
编译器如果期望类型Y
,但是只看到了类型X
,
就会查找implicit函数,将X
转换为Y
。
例如,将一个Double
赋值为Int
就会报错,
scala> val i: Int = 3.5
<console>:11: error: type mismatch;
found : Double(3.5)
required: Int
val i: Int = 3.5
^
但是,如果我们定义一个implicit函数,
将Double
转换为Int
,程序就运行良好了,
scala> implicit def doubleToInt(x: Double) = x.toInt
doubleToInt: (x: Double)Int
scala> val i: Int = 3.5
i: Int = 3
注:
这样做并不是最佳实践,因为doubleToInt
意外损失了精度,
一般而言,对于损失精度的情况最好使用显式转换。
3.2 Converting the receiver
假如我们有一个方法调用obj.doIt
,可是obj
并没有doIt
方法,
编译器就会对obj
进行隐式转换,以期结果对象拥有doIt
方法。
下面我们看两个例子,
例一:Interoperating with new types
假如我们定义了一个Rational
类,
class Rational(n: Int, d: Int){
...
def + (that: Rational): Rational = ...
def + (that: Int): Rational = ...
}
这个Rational
类,有两个重载的+
方法,
分别接受Rational
和Int
类型的参数,
因此,我们可以将一个Rational
与Rational
相加,
还可以将一个Rational
与Int
相加。
scala> val oneHalf = new Rational(1, 2)
oneHalf: Rational = 1/2
scala> oneHalf + oneHalf
res4: Rational = 1/1
scala> oneHalf + 1
res5: Rational = 3/2
但是,程序在执行1 + oneHalf
的时候报错了,
因为1: Int
并没有一个接受Rational
类型作为参数的+
方法。
scala> 1 + oneHalf
<console>:6: error: overloaded method value + with
alternatives (Double)Double <and> ... cannot be applied
to (Rational)
1 + oneHalf
^
为了进行这样的运算,
我们需要定义一个implicit函数,将Int转换为Rational,
scala> implicit def intToRational(x: Int) = new Rational(x, 1)
intToRational: (Int)Rational
scala> 1 + oneHalf
res6: Rational = 3/2
例二:Simulating new syntax
通过对receiver进行隐式转换,我们还可以模拟新的语法,
例如,以下表达式创建了一个Map
对象,
Map(1 -> "one", 2 -> "two", 3 -> "three")
->
看起来很奇怪,但它却并不是一套新的语法,
实际上,->
是ArrowAssoc
类的一个方法,
它在scala.Predef
中定义,
并且其中,还定义了一个implicit函数将Any
转换为ArrowAssoc
,
当我们写1 -> "one"
的时候,编译器会添加一个函数,
将1
转换成ArrowAssoc
。
any2ArrowAssoc(1).->("one")
其中,any2ArrowAssoc
的定义如下,
package scala
object Predef {
class ArrowAssoc[A](x: A){
def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)
}
implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] =
new ArrowAssoc(x)
...
}
这种写法称为rich wrapper模式,
它所提供的功能,仿佛扩展了Scala的语法。
3.3 Implicit parameters
柯里化(curried)函数的,最后一个参数列表,也可以设置为implicit
,
此时,编译器会寻找相应类型的对象,然后进行自动调用。
例如,编译器可能会将someCall(a)
,替换为someCall(a)(b, c, d)
。
这需要,someCall
的最后一个参数列表被标记为implicit
,
还需要,b
,c
,d
相应类型的对象,也被标记为implicit
,且被导入。
下面我们看两个例子,
例一:
class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)
object Greeter {
def greet(name: String)(implicit prompt: PreferredPrompt,
drink: PreferredDrink) {
println("Welcome, "+ name +". The system is ready.")
print("But while you work, ")
println("why not enjoy a cup of "+ drink.preference +"?")
println(prompt.preference)
}
}
object JoesPrefs {
implicit val prompt = new PreferredPrompt("Yes, master> ")
implicit val drink = new PreferredDrink("tea")
}
scala> import JoesPrefs._
import JoesPrefs._
scala> Greeter.greet("Joe")(prompt, drink) // <- 未省略
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master>
scala> Greeter.greet("Joe") // <- 已省略参数
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master>
例二:
def maxListUpBound[T <: Ordered[T]](elements: List[T]): T =
elements match {
case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest =>
val maxRest = maxListUpBound(rest)
if (x > maxRest) x
else maxRest
}
以上我们定义了一个maxListUpBound
函数,
用于获取elements: List[T]
中的最大元素。
其中T <: Ordered[T]
为类型参数T
指定了一个upper bound,
即,T
必须是Ordered[T]
的子类型,
否则,函数体中将无法使用>
方法比较大小。
然而,这样写会有一个弊端,那就是,
对于那些内置类型,例如Int
,它并没有实现为Ordered[Int]
的子类型,
那么该maxListUpBound
函数就不能用于elements: List[Int]
了。
这个问题的一个常见解决方法如下,
def maxListImpParm[T](elements: List[T])
(implicit orderer: T => Ordered[T]): T =
elements match {
case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest =>
val maxRest = maxListImpParm(rest)(orderer)
if (orderer(x) > maxRest) x
else maxRest
}
我们定义了一个类似的函数maxListImpParm
,
它使用了implicit parameter,它隐式传入了一个orderer
函数,
用于将T
类型的对象转换为Ordered[T]
类型。
因此,只要T
可以被转换成Ordered[T]
,
那么该方法就可以被使用,
无需要求T
是Ordered[T]
的子类型。
更妙的是,
orderer
实际上进行了类型转换,
而编译器找到的T => Ordered[T]
类型的对象,也被注明为implicit
的,
因此,orderer
不仅作为implicit parameter来使用,
还可以作为隐式类型转换函数来使用,
所以,我们可以在函数体中,省略对orderer
的调用,让编译器来添加。
def maxList[T](elements: List[T])
(implicit orderer: T => Ordered[T]): T =
elements match {
case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest =>
val maxRest = maxList(rest) // (orderer) is implicit
if (x > maxRest) x // orderer(x) is implicit
else maxRest
}
结果,只有参数列表中出现了orderer
,其余地方都消失了。
因此,orderer
的命名是无关紧要的。
由于这种模式很常见,Scala提供了一个简洁的写法,
def maxList[T <% Ordered[T]](elements: List[T]) : T =
elements match {
case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x
case x :: rest =>
val maxRest = maxList(rest) // (orderer) is implicit
if (x > maxRest) x // orderer(x) is implicit
else maxRest
}
其中,T <% Ordered[T]
中的<%
称为view bound,
指的是,类型T
的对象,可以转换成类型Ordered[T]
的对象,
而且,T
不必是Ordered[T]
的子类型。