【Scala】Scala 隐式转换 implicit
本篇结构:
- 前言
- 隐式转换类型
- 隐式转换的规则 -- 如何寻找隐式转换方法
- 参考博文
一、Implicit 简介
implicit,即隐式转换,是支撑 Scala
易用、容错以及灵活语法的基础。
Scala 的隐式转换系统定义了一套良好的查找机制,当代码出现类型编译错误时,编译器试图去寻找一个隐式 implicit 的转换方法,转换出正确的类型,从而使得编译器能够自我修复,完成编译。
在 Scala
语言当中,隐式转换是一项强大的程序语言功能,它不仅能够简化程序设计,也能够使程序具有很强的灵活性,可以在不修改原有的类的基础上,对类的功能进行扩展,这对一些旧系统的升级十分便利。
通过隐式转换,可以在编写 Scala
程序时故意漏掉一些信息,让编译器去尝试在编译期间自动推导出这些信息来,这种特性可以极大的减少代码量,忽略那些冗长,过于细节的代码。
比如,在 Spark
源码中,经常会发现 RDD
这个类没有 reduceByKey
、groupByKey
等方法定义,但是却可以在 RDD
上调用这些方法。这就是 Scala
隐式转换导致的。
二、隐式转换类型
Scala 隐式转换类型主要包括以下几种类型:隐式参数、隐式试图、隐式类。
2.1、隐式参数
隐式参数是在编译器找不到函数需要某种类型的参数时的一种修复机制。
object ImplicitParam {
def foo(amount: Float)(implicit rate: Float): Unit = {
println(amount * rate)
}
def main(args: Array[String]): Unit = {
// 隐式参数
implicit val r: Float = 0.13F // 定义隐式变量
foo(10) // 输出1.3
}
}
在函数定义的时候,支持在最后一组参数使用 implicit
,表明这是一组隐式参数。在调用该函数的时候,可以不用传递隐式参数,而编译器会自动寻找一个 implict
标记过的合适的值作为该参数。
trait Adder[T] {
def add(x: T, y: T): T
}
implicit val a: Adder[Int] = new Adder[Int] {
override def add(x: Int, y: Int): Int = {
println(x + y)
x + y
}
}
def addTest(x: Int, y: Int)(implicit adder: Adder[Int]): Int = {
adder.add(x, y)
}
addTest(1, 2) // 正确, = 3
addTest(1, 2)(a) // 正确, = 3
addTest(1, 2)(new Adder[Int] {
override def add(x: Int, y: Int): Int = {
println(x - y)
x - y
}
}) // 同样正确, = -1
2.2、隐式视图
隐式视图,是指把一种类型自动转换到另外一种类型,以符合表达式的要求。
隐式视图定义一般用如下形式:implicit def <ConversionName> (<argumentName>: OriginalType): ViewType
。在需要的时候,如果隐式作用域里存在这个定义,它会隐式地把 OriginalType
类型的值转换为ViewType
类型的值。
隐式视图包含两种转换类型:隐式类型转换以及隐式方法调用。
2.2.1、隐式类型转换
隐式类型转换是编译器发现传递的数据类型与申明不一致时,编译器在当前作用域查找类型转换方法,对数据类型进行转换。
举个例子:
object ImplicitView {
def main(args: Array[String]): Unit = {
// 隐式类型转换
implicit def double2Int(d: Double): Int = d.toInt
val i: Int = 3.5
println(i)
}
}
变量i
申明为Int
类型,但是赋值Double
类型数据,显然编译通不过。这个时候可以借助隐式类型转换,定义Double
转Int
规则,编译器就会自动查找该隐式转换,将3.5
转换成3
,从而达到编译器自动修复效果。
2.2.2、隐式方法调用
隐式方法调用是当编译器发现一个对象存在未定义的方法调用时,就会在当前作用域中查找是否存在对该对象的类型隐式转换,如果有,就查找转换后的对象是否存在该方法,存在,则调用。
object ImplicitView2 {
class Horse {
def drinking(): Unit = {
println("I can drinking")
}
}
class Crow {}
object drinking {
// 隐式方法调用
implicit def extendSkill(c: Crow): Horse = new Horse()
}
def main(args: Array[String]): Unit = {
// 隐式转换调用类中不存在的方法
import drinking._
val crow = new Crow()
crow.drinking()
}
}
crow
对象并没有drinkging()
方法定义,但是通过隐式规则转换,可以扩展crow
对象功能,使其可以拥有Horse
对象的功能。
这也是隐式转换最常用的用途:扩展已有的类,在不修改原有类的基础上为其添加新的方法、成员。
2.2.3、隐式视图使用注意
- 不接受多参数
对于隐式视图,编译器最关心的是它的类型签名,即它将哪一种类型转换到另一种类型,也就是说它应该接受只一个参数,对于接受多参数的隐式函数来说就没有隐式转换的功能了。
implicit def int2str(x:Int):String = x.toString // 正确
implicit def int2str(x:Int,y:Int):String = x.toString // 错误
- 不支持嵌套的隐式转换
class A{
def hi: Unit = println("hi")
}
implicit def int2str(x:Int):String = x.toString
implicit def str2A(x:String):A = new A
"str".hi // 正确
1.hi // 错误
- 不能存在二义性,即同一个作用域不能定义两个相同类型的隐式转换函数,这样编译器将无法决定使用哪个转换
/* 错误-- */
implicit def int2str(x:Int):String = x.toString
implicit def anotherInt2str(x:Int):A = x.toString
/* --错误 */
2.3、隐式类
Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换。
object ImplicitClass {
class Crow {}
object crow_eval {
implicit class Parrot(animal: Crow) {
def say(say: String): Unit = {
println(s"I have the skill of Parrot: $say")
}
}
}
def main(args: Array[String]): Unit = {
// 隐式类
import crow_eval._
val crow: Crow = new Crow
crow.say("balabala")
}
}
2.3.1、隐式类使用注意
- 只能在别的 trait/类/对象内部定义,即隐式类必须被定义在类、伴生对象和包对象里
object Helpers {
implicit class RichInt(x: Int) // 正确!
}
implicit class RichDouble(x: Double) // 错误!
- 构造参数有且只有一个,且为非隐式参数
implicit class RichDate(date: java.util.Date) // 正确!
implicit class Indexer[T](collecton: Seq[T], index: Int) // 错误!
implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // 错误!
- 隐式类不能是
case class
(case class
会自动生成伴生对象,与上一条矛盾)
implicit case class Baz(x: Int) // 错误!
- 作用域内不能有与之同名的标识符
三、隐式转换的规则 -- 如何寻找隐式转换方法
Scala 编译器是按照怎样的套路来寻找一个可以应用的隐式转换方法呢? 在Martin Odersky 的 Programming in Scala, First Edition 中总结了以下几条原则:
object StringOpsTest extends App {
// 定义打印操作Trait
trait PrintOps {
val value: String
def printWithSeperator(sep: String): Unit = {
println(value.split("").mkString(sep))
}
}
// 定义针对String的隐式转换方法
implicit def stringToPrintOps(str: String): PrintOps = new PrintOps {
override val value: String = str
}
// 定义针对Int的隐式转换方法
implicit def intToPrintOps(i: Int): PrintOps = new PrintOps {
override val value: String = i.toString
}
// String 和 Int 都拥有 printWithSeperator 函数
"hello,world" printWithSeperator "*"
1234 printWithSeperator "*"
}
3.1、标记规则
只会去寻找带有implicit
标记的方法,这点很好理解,在上面的代码也有演示,如果不申明为implicit
,只能手工去调用。
3.2、作用域范围规则
只会在当前表达式的作用范围之内查找,而且只会查找单一标识符的函数,上述代码中,如果stringToPrintOps
方法封装在其他对象(比如叫Test)中,虽然Test
对象也在作用域范围之内,但编译器不会尝试使用Test.stringToPrintOps
进行转换,这就是单一标识符的概念。
单一标识符有一个例外,如果stringToPrintOps
方法在PrintOps
的伴生对象中申明也是有效的,Scala 编译器也会在源类型或目标类型的伴生对象内查找隐式转换方法,本规则只会在转型有效。而一般的惯例,会将隐式转换方法封装在伴生对象中。
当前作用域上下文的隐式转换方法优先级高于伴生对象内的隐式方法。
3.3、不能有歧义原则
在相同优先级的位置只能有一个隐式的转型方法,否则Scala编译器无法选择适当的进行转型,编译出错。
3.4、只应用转型方法一次原则
Scala编译器不会进行多次隐式方法的调用,比如需要C
类型参数,而实际类型为A
,作用域内存在A => B
,B => C
的隐式方法,Scala编译器不会尝试先调用A => B
,再调用B => C
。
3.5、显示方法优先原则
如果方法被重载,可以接受多种类型,而作用域中存在转型为另一个可接受的参数类型的隐式方法,则不会被调用,Scala编译器优先选择无需转型的显式方法。
def m(a: A): Unit = ???
def m(b: B): Unit = ???
val b: B = new B
//存在一个隐式的转换方法 B => A
implicit def b2a(b: B): A = ???
m(b) //隐式方法不会被调用,优先使用显式的 m(b: B): Unit