本章学习隐式转换和隐式参数,隐式转换和隐式参数是Scala两个功能强大的工具,在幕后处理那些很好有价值的工作。属于Scala高级阶段知识,初学者只需了解其用法即可,不用太过深入。
一、隐式转换
1.基本概念
隐式转换指的是那种以implicit关键字声明的带有单个参数的函数。这样的函数将被自动应用,将一种类型转为另一种类型。
①应用案例
首先看一个简单的案例,定义一个整数类型Int的变量,赋值Double类型:
val num: Int = 4.5 //nun为Int类型,赋值4.5 Double类型
执行结果:
从执行结果发现,并不能编译通过,不能将Double类型的值 赋值给Int类型。
这时,我们可以使用隐式转换函数优雅的解决上述数据类型转换问题。
首先需要编写一个隐式转换函数Doule2Int:
implicit def Doule2Int(d : Double): Int = d.toInt
执行结果:
可以看到,val num: Int = 4.5
赋值成功。
②源码分析
为了方便反编译,我们创建一个scala源文件:
object DemoImplicit{
def main(args:Array[String]): Unit = {
implicit def Double2Int(d: Double): Int = d.toInt
val num: Int = 3.5
}
}
反编译后的代码:
public final class DemoImplicit$
{
public static DemoImplicit$ MODULE$;
static {
new DemoImplicit$();
}
public void main(final String[] args) {
final int num = Double2Int$1(3.5);
Predef$.MODULE$.println((Object)BoxesRunTime.boxToInteger(num));
}
private static final int Double2Int$1(final double d) {
return (int)d;
}
private DemoImplicit$() {
DemoImplicit$.MODULE$ = this;
}
}
从反编译的源码可以看到val num: Int = 3.5
底层调用了Double2Int$1(3.5)
,而Double2Int$1
方法return (int)d
,即对传入Double类型的d 做了int强转。
③细节问题
- 隐式转换函数的函数名可以是任意的,隐式转换与函数名称无关,只与函数参数类型和返回值类型有关。
- 隐式函数可以有多个(即:隐式函数列表),但是需要保证在当前环境下,只有唯一个隐式函数能被识别。
- Scala在隐式转换的时候可能有潜在的问题,为了避免在使用隐式函数时出现警告,我们可以添加
import scala.language.implicitConversions
2.隐式转换丰富类库功能
隐式转换的另一个作用是:可以丰富类库的功能。比如你希望你使用的某个类有某个方法,但是这个类却没有提供。Java没有办法解决,但Scala允许你定义一个经过丰富的类型,添加你想要的功能。
例如:如果你希望java.io.File类
能有个read方法来读取文件:
val contents = new Flie("README").read
我们可以定义一个经过丰富的类型,添加read方法:
class RichFile(val from: File){
def read = Source.fromFile(from.getPath).mkString
}
然后再提供一个隐式转函数来 将原来的File类型 转换到新的RichFile类型:
implicit def file2RichFile(from: File) = new RichFile(from)
这样,我们就可以在File对象上调用read方法了。
除了提供隐式转换函数,我们也可以提供一个隐式转换类:
implicit class RichFile(val from: File){
def read = Source.fromFile(from.getPath).mkString
}
3.引入隐式转换
Scala会考虑如下的隐式转换函数:
- 位于源或者目标类型的伴生对象中的隐式函数。
- 位于当前作用域可以用单个标识符指代隐式函数。
4.隐式转换规则
编译器在何时会尝试隐式转换? 隐式转换在以下三种情况会被考虑:
- 1.当表达式的类型与预期的类型不同时。 例如:
val num: Int = 4.5
编译器会寻找是否有Double => Int类型的隐式转换函数。 - 2.当对象访问一个不存在的成员时。
- 3.当对象调用某个方法, 而方法的参数与传入的参数不匹配时。
对应的,Scala编译器在以下三种情况不会考虑尝试隐式转换:
- 1.如果代码能够在不适用隐式转换的前提下通过,则不会使用隐式转换。例如:如果
a*b
能过通过,就不会尝试a *convert(b)
或者b.convert(a)
。 - 2.不会嵌套的执行隐式转换函数。即不会执行
conver1(convert2(a))*b
。 - 3.存在二义性的转换函数不会执行。只能存在一个唯一确定的转换函数。例如,如果
convert1(a)*b
和convert2(a)*b
都存在且合法,那么在执行a*b
时,就不会尝试隐式转换。
二、隐式参数
隐式参数也叫隐式变量,将某个形参变量标记为implicit。在这种情况下,编译器将会查找默认值,提供给本次函数调用。
示例:
case class Delimiters(left: String, right: String)
def quote(what: String)(implicit delims: Delimiters) = {
delims.left + what + delims.right
}
假设不用隐式参数,我们可以用一个显示的Delimiters对象来调用quote方法:
quote("I Love You")(Delimiters("<<", ">>")) //返回结果:<<I Love You>>
如果我们省略隐式参数列表,这样调用quote方法:
quote("I Love You")
在这种情况下,编译器会查找一个类型为Delimiters的隐式值。 这个隐式值必须是被implicit声明的值。
编译器会在以下两个地方查找这样的一个对象:
- ①当前作用域所有用单个标识符指代的满足类型要求的def 和 val
- ②满足要求类型相关联的类型的伴生对象中。相关联的类型包括所要求类型本身,以及它的参数类型。
在本例中,我们就可以使用第②中情况:创建一个伴生对象,然后创建一个Delimiters的类型参数。
object MyDelimiters{
implicit val quoteDelimites = Delimiters("<<",">>")
}
然后从这个对象中引入所有值或指定值:
import MyDelimiters._
或者import MyDelimiters.quoteDelimiters
这样一来执行quote("I Love You")
会隐式的获得参数("<<",’’>>’’)提供给quote调用。
使用隐式参数的完整代码:
object DemoImplicit{
case class Delimiters(left: String, right: String)
def quote(what: String)(implicit delims: Delimiters) = {
delims.left + what + delims.right
}
//定义主函数,方便调用 quote
def main(args:Array[String]): Unit = {
import MyDelimiters._ //引入隐式参数
val str: String = quote("I Love You") //隐式调用
println(str)
}
}
//
object MyDelimiters{
implicit val quoteDelimites = Delimiters("<<",">>")
}
三、隐式类
在scala2.10后提供了隐式类,可以使用implicit声明类,隐式类的非常强大,同样可以扩展类的功能,比前面使用隐式转换丰富类库功能更加的方便,在集合中隐式类会发挥重要的作用。
隐式类使用有如下几个特点:
- 隐式类所带的构造参数有且只能有一个
- 隐式类必须被定义在“类”或“伴生对象”或“包对象”里,即隐式类不能是 顶级的(top-level objects)。
- 隐式类不能是case class
- 作用域内不能有与之相同名称的标识符
四、总结
隐式定义是 Scala 的一项强大的、可以浓缩代码的功能。但是值得注意的是:隐式转换如果使用得过于频繁,会让代码变得令人困惑。因此,在添加一个新的隐式转换之前,首先问自己能否通过其他
手段达到相似的效果,比如继承、混人组合或方法重载。不过,如果所有这些都失败了,而你感觉大量代码仍然是繁复冗长的。那么隐式转换可能恰好能帮到你。
隐式转换还涉及 泛型、上下界定等等内容,暂时用不上。