隐式转换和隐式参数
- 如果使用别人的代码库,无法进行修改,Scala进行扩展的方法是隐式转换和隐式参数。允许省略掉冗余且明显的细节。
隐式转换
- 隐式转换通常在两个开发完全不知道对方存在的软件或类库时非常有用。如果双方都描述了同一样概念,隐式转换可以减少从一个类型显式转换成另一个类型的需要。
隐式规则
- 隐式定义指的是那些我们允许编译器插入程序以解决类型错误的定义。如果
x + y
不能通过编译,编译器可能会改为convert(x) + y
,其中convert
是某种可用的隐式转换,如果convert(x)
的对象支持+
动作,那么这个改动就可能修复程序,让它通过类型检查并正确运行。如果convert
真的是某种简单的转换函数,可以不显式地写出这个方法。
- 隐式定义指的是那些我们允许编译器插入程序以解决类型错误的定义。如果
-
- 隐式转换受如下规则的约束:
- 标记规则:只有标记为
implicit
的定义才可用。implicit
用来标记哪些声明可被编译器用作隐式定义。编译器只会从那些显式标记为implicit
的定义中选择。
- 标记规则:只有标记为
-
- 作用域规则:被插入的隐式转换必须是当前作用域的单个标识符,而且跟隐式转换的源类型或者目标类型有关联。必须将隐式转换引入到当前作用域才能使得它们可用,例如,编译器不会引入
varaiable.convert
这种语法,必须要引入它,使其成为单个标识符。
- 2.1. 编译器会在隐式转换的源类型或目标类型的伴生对象中查找隐式定义。例如尝试将
Dollar
对象传递给一个接收Euro
的函数,编译器会在Dollar
和Euro
的伴生对象中寻找隐式转换。在伴生对象中定义的隐式转换可以不引入直接使用。就是隐式定义是在当前作用域有效的,而不是全部有效。
- 作用域规则:被插入的隐式转换必须是当前作用域的单个标识符,而且跟隐式转换的源类型或者目标类型有关联。必须将隐式转换引入到当前作用域才能使得它们可用,例如,编译器不会引入
- 每次一个规则:每次只能有一个隐式定义被插入。
- 显式优先原则:只要代码按照编写的样子能够通过类型检查,就不会启动隐式转换。
- 命名一个隐式转换:隐式转换可以使用任何名称,名称并不重要,只有在显式引入该转换的时候使用以及决定在程序的某个位置都有哪些隐式转换可用时用到。例如,在一个对象中有两个隐式转换,
intToString
,StringToStringWrapper
,如果只想将String
转换为StringWrapper
,而不想将Int
转换为String
,可以只引入第二个隐式转换。
- 命名一个隐式转换:隐式转换可以使用任何名称,名称并不重要,只有在显式引入该转换的时候使用以及决定在程序的某个位置都有哪些隐式转换可用时用到。例如,在一个对象中有两个隐式转换,
- 4.尝试隐式转换的地方:1.转换到一个预期的类型;2.对某个选择接收端的转换;3.隐式参数
A. 隐式转换到一个预期的类型
- 当编译器发现
X
而需要Y
的时候,查找能够将X
转换为Y
的隐式转换。注意隐式转换的引入需要在使用之前,不然编译器不会发现这个隐式转换。scala> val i: Int = 3.5 <console>:7: error: type mismatch; scala> implicit def doubleToInt(x: Double) = x.toInt doubleToInt: (x: Double)Int //这个隐式转换需要放在定义语句之前。
doubleToInt
是单个标识符,如果不是定义在当前作用域中,可以使用import
或者extend
或者with
特质来导入。类似Double
转向Int
这种通用类型转向受限类型的转换会丢失精度,但是反方向的转换是定义得通的,需要自己定义。在Predef
中有从Int
到Double
的转换。
B. 转换接收端
隐式转换还能应用关于方法调用的接收端,也就是方法被调用的那个对象。
-
这个隐式转换主要有两种用途,1.接收端转换允许我们更平滑地将新类继承到已有的类继承关系图谱中,2.支持在语言中编写领域特定语言
(DSL)
。假如写下obj.doIt
,但是obj
中并不存在名为doIt
的成员,编译器会在放弃之前尝试插入转换。在本例中,这个转换需要应用于接收端,也就是obj
,编译器会寻找obj
到预期类型的转换,这个预期类型中拥有名为doIt
的成员。1. 与新类型互操作
- 接收端转换的一个主要用途是让新类型和已有类型的集成更顺滑。可以让客户端的代码不改变,就像是在使用新类型一样。
1 + new Rational(3,4)
可插入一个隐式转换,最后变成intToRational(1) + new Rational(3,4)
,完美解决Int
类型中没有+(rational: Rational)
这个方法。
2. 模拟新的语法
- 隐式转换的另一个用途是模拟添加新的语法,例如
Map
中的语法:Map(1 -> "1")
,整体的操作过程是这样的。编译器插入any2ArrowAssoc
的转换,将1
转换为带有->
方法的ArrowAssoc
的对象,从而可以调用->
,这看起来就像是一个新的语法。如果某个对象调用了不属于自己的方法,那么很有可能是使用了隐式转换。可以使用这些富包装类模式做出以类库形式定义的内部DSL
。
3. 隐式类
-
Scala 2.10
引入了隐式类来简化富包装类的编写。隐式类是以implicit
打头的类,对于这样的类,编译器会生成一个从类的构造方法参数到类本身的隐式转换。例如,
case class Rectangle(width: Int, height: Int)
如果经常使用这个类,可以使用富包装类来简化构造工作,定义:
implicit class RectangleMaker(width: Int) { def x(height: Int) = Rectangle(width, height) } // Automatically generated implicit def RectangleMaker(width: Int) = new RectangleMaker(width)
对于以上的隐式类,会自动生成类构造参数到该类对象的一个转换。可以直接使用
3 x 4
这样的形式。scala> val myRectangle = 3 x 4 myRectangle: Rectangle = Rectangle(3,4)
并不是任何类定义前面度可以放
implicit
,隐式类不能是样例类,并且其构造方法必须有且仅有一个参数。隐式类必须存在于一个对象、类或者特质里面。 - 接收端转换的一个主要用途是让新类型和已有类型的集成更顺滑。可以让客户端的代码不改变,就像是在使用新类型一样。
C. 隐式参数
- 编译器会插入隐式定义的最后一个地方是参数列表,编译器有时候会将
someCall(a)
替换为someCall(a)(b)
,通过追加一个参数列表来完成某个函数的调用。隐式参数提供的是整个最后一组currying
的参数列表,而不仅仅是最后一个参数。例如,someCall(a)
可能根据实际情况被替换为someCall(a)(b, c, d)
。如果要让编译器隐式地填充隐式参数,首先需要定义这样一个符合预期类型的变量。填充的变量也必须声明为implicit
的,如果不是这样,编译器不会使用它来填充缺失的列表。如果变量不是当前作用域内的单个标识符,也不会被采纳。implicit
关键字是应用到整个参数列表而不是单个参数的。def greet(name: String)(implicit prompt: PreferredPrompt, drink: PreferredDrink) = {}
- 由于编译器是在作用域内通过类型匹配来填充隐式参数,所以一般希望类型能够特殊从而避免误匹配的出现。隐式参数最常使用的场景是提供关于在更靠前的参数列表中已经显式提到的类的信息。
def maxListOrdering[T](elements: List[T])(implicit ordering: Ordering[T]): T
ordering
说明了已知类型T
更多的信息。有了T
,ordering
的类型就已知,可以使用隐式参数隐式插入。
- 隐式参数的代码风格,最好是对隐式参数使用定制名称的类型,比如
PreferredPrompt
和PreferredDrink
,而不是String
和String
,而且这个类型命名时,至少使用一个能明确其智能的名字,比如Ordering
。如果定义成为implicit (T,T) => T
,该参数并没有透露出任何关于该烈性用途的信息,范围太广,很容易误使用。
上下文界定
- 当一个参数是隐式定义的时候,在函数体内又使用了这个隐式参数作为参数传递,这时可以将不用写这个参数。
def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = { val maxRest = maxListOrdering(rest)(ordering) //这个ordering可以省略 } def maxListOrdering[T](elements: List[T])(ordering: Ordering[T]): T = { if (ordering.gt(x, maxRest)) x else maxRest //为了避免使用ordering, //可以使用库函数 > implicit[Ordering[T]],该函数返回的是Ordering[T]的隐式对象。 //这样ordering就可以随意命名。 }
Scala
允许声调这个参数的名称并使用上下文界定来缩短方法签名。[T: Ordering]
是一个上下文界定,context bound
,做了两件事情。1. 引入类型参数T
,2.添加一个Ordering[T]
的隐式参数,并不需要知道这个参数的名字。最后的代码如下:def maxList[T : Ordering](elements: List[T]): T = elements match { case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxList(rest) if (implicitly[Ordering[T]].gt(x, maxRest)) x else maxRest }
当有多个转换可选时
- 如果有多个转换可选,
Scala
会拒绝插入,隐式转换在这个转换是显而易见的并且纯粹是样板代码的时候最好用。Scala
目前采取的措施是使用可用转换中更具体的转换,具体体现在,该转换的入参类型是别的转换的子类型;两者都是方法,具体转换所在的类扩展自通用转换所在的类。
调试
- 调试的时候可以显示地写出来转换用以明确编译器插入的转换到底是哪一个。也可以对编译器设置选项
-Xprint:typer
,使用这个选项来运行scalac
,可以看到添加了隐式转换之后的代码。使用的方法:scalac -Xprint:typer test.scala
。隐式转换还是不能滥用的。