1.为什么要隐式转换
我们经常引入第三方库,但当我们想要扩展新功能的时候通常是很不方便的,因为我们不能直接修改其代码。scala提供了隐式转换机制和隐式参数帮我们解决诸如这样的问题。
Scala中的隐式转换是一种非常强大的代码查找机制。当函数、构造器调用缺少参数或者某一实例调用了其他类型的方法导致编译不通过时,编译器会尝试搜索一些特定的区域,尝试使编译通过。
2. 优点
通过隐式转换,程序员可以在编写Scala程序时故意漏掉一些信息,让编译器去尝试在编译期间自动推导出这些信息来,这种特性可以极大的减少代码量,忽略那些冗长,过于细节的代码。
3. 概念
简单说,隐式转换就是:当Scala编译器进行类型匹配时,如果找不到合适的候选,那么隐式转化提供了另外一种途径来告诉编译器如何将当前的类型转换成预期类型。
4。隐式转换的规则:
-
如果代码无需隐式转换即可通过编译,则不会引入隐式转换
比如scala中如果有类似java自动装箱的功能,int类型可以自动转化为integer,那么如果你写一个隐士转换,将int转换成integer,那么这个隐式转换将不会被调用。
-
标记规则:只有标记为implicit的变量,函数或对象定义才能被编译器当做隐式操作目标。
-
作用域规则:插入的隐式转换必须是单一标示符的形式处于作用域中,或与源/目标类型关联在一起。单一标示符是说当隐式转换作用时应该是这样的形式:file2Array(arg).map(fn)的形式,而不是foo.file2Array(arg).map的形式。假设file2Array函数定义在foo对象中,我们应该通过import foo._或者import foo.file2Array把隐式转换导入。简单来说,隐式代码应该可以被"直接"使用,不能再依赖类路径。假如我们把隐式转换定义在源类型或者目标类型的伴生对象内,则我们可以跳过单一标示符的规则。因为编译器在编译期间会自动搜索源类型和目标类型的伴生对象,以尝试找到合适的隐式转换。
-
无歧义规则:存在二义性的隐式转换报错
不能存在多于一个隐式转换使某段代码编译通过。因为这种情况下会产生迷惑,编译器不能确定到底使用哪个隐式转换。
比如转化String为int的隐式转换,你定义了两个,A和B,那么就不报错,就像你班级里有两个相同名字的学生。老师点名 鬼知道具体是哪个
-
单一调用规则:隐式转换只会匹配一次,即隐式转换至多发生一次
不会叠加(重复嵌套)使用隐式转换。一次隐式转化调用成功之后,编译器不会再去寻找其他的隐式转换。
-
显示操作优先规则:当前代码类型检查没有问题,编译器不会尝试查找隐式转换。
如果没有第5条,我将会有个疑问?假设我们第一次隐式转换把int变成String,隐式转换是
implicit def int2String(x : Int) = x.toString
一个Int=>String的隐式转换,import后,Int会被编译器替换为String
会替换,那么如果没第二条规则,我们就惨了,从引入隐式转换开始,到我们程序结束,都是Int会被编译器替换为String,也就是说没有String类型了,int类型有俩了,一个是"10",一个是10,想想多么可怕。所以发生一次是比较好的。想多次使用的时候我们可以多次引用。
5。隐式解析的搜索范围
这一部分的规则有些复杂,根据《Scala In Depth》所描述的,顶层的搜索逻辑是:
- 在当前作用域下查找。这种情形又分两种情况,一个是在当前作用域显示声明的implicit元素,另一个通过import导入的implicit元素。
- 如果第一种方式没有找到,则编译器会继续在隐式参数类型的隐式作用域里查找。
真正复杂的地方是什么叫一个类型的隐式作用域?一个类型的隐式作用域指的是“与该类型相关联的类型”的所有的伴生对象。
OK,那什么叫作“与一个类型相关联的类型”? 定义如下:
- 假如T是这样定义的:T with A with B with C,那么A, B, C的伴生对象都是T的搜索区域。
- 如果T是类型参数,那么参数类型和基础类型都是T的搜索部分。比如对于类型List[Foo],List和Foo都是搜索区域
- 如果T是一个单例类型p.T,那么p和T都是搜索区域
- 如果T是类型注入p#T,那么p和T都是搜索区域。
隐式转换有新旧两种定义方法,旧的定义方法指是的“implict def”形式,这是Scala 2.10版本之前的写法,在Scala 2.10版本之后,Scala推出了“隐式类”用来替换旧的隐式转换语法,因为“隐式类”是一种更加安全的方式,对被转换的类型来说,它的作用域更加清晰可控。
接下来我们通过实例来了解这两种隐式转换的写法。前文提到,隐式转换最为基本的使用场景是:将某一类型转换成预期类型,所以我们下面的例子就以最这种最简单的场景来演示,它们都实现了:将一个String类型的变量隐式转换为Int类型:
简单查找 看列子
class Implicits {
implicit val content = "Java Hadoop"
}
object Implicits {
implicit val content = "Scala Spark"
}
object ImplicitsAdvanced {
def main(args: Array[String]): Unit = {
def printContent(implicit content: String) = println(content)
implicit val content = "I love spark"
import Implicits._
printContent
}
printContent 的隐式参数是String类型,所以会找String类型的隐式参数,
上面例子上有三个可能,“Java Hadoop”, “Scala Spark”, “I love spark”
结果是
I love spark
即它会先在main这个大括号里找满足要求的String类型隐式参数,找到就用,优先级最高。
只有找不到了才会去导入的类里找,把implicit val content = "I love spark"注释掉后
结果
Scala Spark
Scala Spark 它要找的是伴生对象里的隐式参数,而非类里面的隐式参数!
如果再加上一个隐式参数如下
object ImplicitsMsg {
implicit val content = "Kafak Zookeeper"
}
导入时这样导入
import ImplicitsMsg.content
结果是
Kafak Zookeeper
说明都是导入的情况下,优先选择具体的,通配符导入的优化级较低。
6。隐式转换的时机
- 当方法中的参数的类型与目标类型不一致时
- 当对象调用类中不存在的方法或成员时,编译器会自动将对象进行隐式转换
7.隐式转换有四种常见的使用场景:
- 将某一类型转换成预期类型
- 类型增强与扩展
- 模拟新的语法
- 类型类
使用方式:
1.将方法或变量标记为implicit
2.将方法的参数列表标记为implicit
3.将类标记为implicit
Scala支持两种形式的隐式转换:
隐式值:用于给方法提供参数
隐式视图:用于类型间转换或使针对某类型的方法能调用成功
8。先看一个例子 “implict def”形式的隐式转换
package source.mscala
object ImplicitDefDemo {
object MyImplicitTypeConversion {
implicit def strToInt(str: String) = str.toInt
}
def main(args: Array[String]) {
//compile error!
val max = math.max("1", 2);
/*import MyImplicitTypeConversion.strToInt
val max = math.max("1", 2);*/
println(max)
}
}
运行直接报错
Error:(11, 20) overloaded method value max with alternatives:
(x: Double,y: Double)Double <and>
(x: Float,y: Float)Float <and>
(x: Long,y: Long)Long <and>
(x: Int,y: Int)Int
cannot be applied to (String, Int)
val max = math.max("1", 2);
然后修改
package source.mscala
object ImplicitDefDemo {
object MyImplicitTypeConversion {
implicit def strToInt(str: String) = str.toInt
}
def main(args: Array[String]) {
//compile error!
// val max = math.max("1", 2);
import MyImplicitTypeConversion.strToInt
val max = math.max("1", 2);
println(max)
}
}
运行结果正确
2
Process finished with exit code 0
这里有疑问?为什么加入import MyImplicitTypeConversion.strToInt这一句代码就可以了呢?
9。隐式值(隐式参数)
假设不引入隐式值试试,( 其中试试了print(person()) 这样编译都会报错)
package source.mscala
object ImplicitDefDemo {
def person(implicit name : String) = name
def main(args: Array[String]) {
print(person)
}
}
直接报错
Error:(6, 11) could not find implicit value for parameter name: String
print(person)
Error:(6, 11) not enough arguments for method person: (implicit name: String)String.
Unspecified value parameter name.
print(person)
然后修改,引入隐式值
package source.mscala
object ImplicitDefDemo {
def person(implicit name : String) = name
def main(args: Array[String]) {
implicit val p = "mobin" //p被称为隐式值
print(person)
}
}
运行结果正确
mobin
Process finished with exit code 0
假设我如果传入了值呢?这时候使用的是哪种在呢?
package source.mscala
object ImplicitDefDemo {
def person(implicit name : String) = name
def main(args: Array[String]) {
implicit val p = "mobin" //p被称为隐式值
print(person("aa"))
}
}
运行结果是我们传入的值,隐式转换没起作用,这里符合了 隐式转换的规则中的第一条和第六条
aa
Process finished with exit code 0
如果我们定义了两个隐式值呢?
package source.mscala
object ImplicitDefDemo {
def person(implicit name : String) = name
def main(args: Array[String]) {
implicit val p = "mobin" //p被称为隐式值
implicit val myp2 = "i am two" //myp2被称为隐式值
print(person)
}
}
会报错
Error:(9, 11) ambiguous implicit values:
both value myp2 of type String
and value p of type String
match expected type String
print(person)
Error:(9, 11) could not find implicit value for parameter name: String
print(person)
Error:(9, 11) not enough arguments for method person: (implicit name: String)String.
Unspecified value parameter name.
print(person)
这里的意思是,print(person)因为你没传入参数,而person(implicit name : String) 需要的隐式参数是String类型的,所以去找隐式值,然后根据搜索范围,根据第一条 在当前作用域下查找。这种情形又分两种情况,一个是在当前作用域显示声明的implicit元素,另一个通过import导入的implicit元素。这里有两个String类型的,因此两个都符合,但是编译器不知道调用哪个啊,所以报错。
假设是不同的类型呢?
package source.mscala
object ImplicitDefDemo {
def person(implicit name : String) = name
def main(args: Array[String]) {
implicit val p:String = "mobin" //p被称为隐式值 String类型
implicit val myp2:Int = 3 //myp2被称为隐式值 int类型
print(person)
}
}
运行结果正确,虽然在一个搜索范围里有两个隐式值,但是类型不符合
mobin
Process finished with exit code 0
10。隐式视图,(隐式函数)
隐式转换为目标类型:把一种类型自动转换到另一种类型
package source.mscala
object ImplicitDefDemo {
def foo(msg : String) = println(msg)
implicit def intToString(x : Int) = x.toString
def main(args: Array[String]) {
foo(10)
}
}
如果没有 implicit def intToString(x : Int) = x.toString这个函数,是无法编译的,直接就是错误
11。隐式转换调用类中本不存在的方法
package source.mscala
class SwingType{
def wantLearned(sw : String) = println("兔子已经学会了"+sw)
}
object swimming{
implicit def learningType(s : AminalType) = {
println("i am run ")
new SwingType
}
}
class AminalType
object AminalType {
def main(args: Array[String]) {
import source.mscala.swimming._
val rabbit = new AminalType
rabbit.wantLearned("breaststroke") //蛙泳
}
}
如果没有import source.mscala.swimming._,那么无法编译
编译器在rabbit对象调用时发现对象上并没有wantLearning方法,此时编译器就会在作用域范围内查找能使其编译通过的隐式视图,找到learningType方法后,编译器通过隐式转换将对象转换成具有这个方法的对象,之后调用wantLearning方法
运行结果
i am run
兔子已经学会了breaststroke
可以将隐式转换函数定义在伴生对象中,在使用时导入隐式视图到作用域中即可(如例4的learningType函数)
package source.mscala
class SwingType{
def wantLearned(sw : String) = println("兔子已经学会了"+sw)
}
package swimmingPage{
object swimming{
implicit def learningType(s : AminalType) = {
println("i am run ")
new SwingType
}
}
}
class AminalType
object AminalType {
def main(args: Array[String]) {
import source.mscala.swimmingPage.swimming._
val rabbit = new AminalType
rabbit.wantLearned("breaststroke") //蛙泳
}
}
运行结果和上面相同,但是有什么区别呢?没感觉多大区别
像intToString,learningType这类的方法就是隐式视图,通常为Int => String的视图,定义的格式如下:
implicit def originalToTarget ( : OriginalType) : TargetType
其通常用在于以两种场合中:
1.如果表达式不符合编译器要求的类型,编译器就会在作用域范围内查找能够使之符合要求的隐式视图。如例2,当要传一个整数类型给要求是字符串类型参数的方法时,在作用域里就必须存在Int => String的隐式视图
2.给定一个选择e.t,如果e的类型里并没有成员t,则编译器会查找能应用到e类型并且返回类型包含成员t的隐式视图。如例3
12。隐式类
在scala2.10后提供了隐式类,可以使用implicit声明类,但是需要注意以下几点:
- 其所带的构造参数有且只能有一个
- 隐式类必须被定义在类,伴生对象和包对象里
- 隐式类不能是case class(case class在定义会自动生成伴生对象与2矛盾)
- 作用域内不能有与之相同名称的标示符
package source.mscala
object Stringutils {
implicit class StringImprovement(val s : String){ //隐式类
def increment = s.map(x => (x +1).toChar)
}
}
object Main extends App{
import source.mscala.Stringutils._
println("mobin".increment)
}
编译器在mobin对象调用increment时发现对象上并没有increment方法,此时编译器就会在作用域范围内搜索隐式实体,发现有符合的隐式类可以用来转换成带有increment方法的StringImprovement类,最终调用increment方法。运行结果如下
npcjo
Process finished with exit code 0