隐式定义是指编译器为了修正类型错误而允许插入到程序中的定义。
举例:
正常情况下"120"/12
显然会报错,因为 String 类并没有实现 / 这个方法,我们无法去决定 String 类的行为,这个时候就可以用上 implicit 关键字了。
使用 implicit 关键字定义函数。
implicit def String2Int(str: String) = {
str.toInt
}
print("120" / 12)
编译器一旦发现对于 String 类操作符 / 不可用,而 + 方法正好对应 Int 型的参数,且当前作用域存在 String 类型的隐式转换。
所以实际等价于print(String2Int("120") / 12)
。
隐式操作规则
作用域规则:插入的隐式转换必须以单一标识符的形式处于作用域中,或与转换的源或目标类型关联在一起。
Scope Rule: An inserted implicit conversion must be in scope as a single identifier, or be associated with the source or target type of the conversion.
Scala 编译器仅考虑处于作用域之内的隐式转换。可以使用 import 关键字访问其它库的隐式转换。
编译器还将在源类型或转化的期望目标类型的伴生对象中寻找隐式定义。
case class Euro(num: Int)
object Dollar {
implicit def EuroToDollar(euro: Euro): Dollar = Dollar(2 * euro.num)
}
case class Dollar(num:Int) {
def +(d: Dollar) = num + d.num
}
print(Dollar(3) + Euro(10)) // 23
无歧义规则:隐式转换唯有在不存在其它可插入转换的前提下才能插入。
Non-Ambiguity Rule: An implicit conversion is only inserted if there is no other possible conversion to insert.
implicit def String2Int(str: String) = {
str.toInt
}
implicit def String2Double(str: String) = {
str.toDouble
}
print("120" / 12)
上面的程序会导致编译错误。可以想象,如果不禁止这种操作势必会导致可读性的下降。
单一调用原则:只会尝试一个隐式操作。
One-at-a-time Rule: Only one implicit is tried.
编译器不会对某个变量执行多次的隐式转换。
显式操作先行原则:若编写的代码类型检查无误,则不会尝试任何隐式操作。
Explicits-First Rule: Whenever code type checks as it is written, no implicits are attempted.
implicit def String2Int(str: String) = {
str.toInt
}
print("120" + 12) // 12012
120
并没有被转换为 Int 类型,而是 12
被转换成了 String 类型。
也就是说编译器并不会优先考虑我们定义的隐式操作。
命名隐式转换
Naming an implicit conversion.
object MyConversions {
implicit def String2Int(str: String) = str.toInt
implicit def Double2Int(num: Double) = num.toInt
}
import MyConversions.Double2Int
val x:Int = 12.0
print(x)
这样可以很方便的在当前作用域内引入我们需要的隐式转换。
隐式转换
与新类型的交互操作
现在我们有一个 Person 类。
case class Person(name: String, age: Int) {
def +(x: Int) = age + x
def +(p: Person) = age + p.age
}
其中定义了 + 方法。
val person = Person("xiaohong", 1)
println(person + 1) // 2
但是反过来 println(1 + person)
就不行了。(整数类型显然没有合适的 + 方法)
这个时候定义从 Int 到 Person 的隐式转换就很方便了。
implicit def Add1(x: Int) = Person("Empty", x)
println(1 + person) // 2
当然也可以定义从 Person 到 Int 的隐式转换。
implicit def Add2(x: Person) = x.age
println(1 + person) // 2
但是如果我们同时定义了这两个函数,看看会发生什么?
implicit def Add1(x: Int) = Person("Empty", x)
implicit def Add2(x: Person) = x.age + 1 // 加以区分
println(1 + person) // 3
实际上 + 操作的原型是 1.+(person)
,person 作为参数。所以 person 被转化成了 Int 类型参与计算。
模拟新的语法
我们有很多种方法去创建一个 Map 对象。
var mp = Map(1 -> 2, 3 -> 4)
var mp2 = Map(Tuple2(1, 2), Tuple2(3, 4))
var mp3 = Map((1, 2), (3, 4))
var mp4 = Map(1 → 2, 3 → 4)
print(mp == mp2)
print(mp == mp3)
print(mp2 == mp3)
print(mp == mp4)
到今天才知道 -> 符号是如何支持这种操作的。
去 Predef 库中找到 -> 的实现如下:
implicit final class ArrowAssoc[A](private val self: A) extends AnyVal {
@inline def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
def →[B](y: B): Tuple2[A, B] = ->(y)
}
并不是什么内建语法,其实就是基于隐式转换。
现在自己定义一个符号 -->
implicit class MyRange(start: Int) {
def -->(end: Int) = start to end
}
print((1 --> 10).sum) // 55
隐式参数
在一个方法的参数名前加上 implicit 关键字。
implicit val a = 2
implicit val b = "B"
def fun(implicit x: Int, y:String) = {
x + y
}
fun // 2B
fun(1, "A") // 1A
如果我们不提供相应的参数,那么方法会自动带入当前作用域内的带有 implicit 关键字的变量作为参数。编译器会根据类型去匹配。
总结
隐式操作是功能强大、代码凝练的 Scala 特性。无论是标准库内还是其它开源的框架,都大量的使用了这一特性。但是它的频繁使用必然会导致代码可读性的大幅度降低,当然我针对的是库的使用者以及两个月后再看库的开发者。(越来越感觉 Scala 和 Java 走了两个极端)
参考