一、隐式转换
1.1 提出问题
先看一段代码,引出隐式转换的实际需要——指定某些数据类型的相互转化:
packagecom.atguigu.scala.conversionobjectScala01{defmain(args:Array[String]):Unit={valnum:Int=3.5// ? 错!高精度 -> 低精度println(num)}}在Scala中,将高精度(如Double)赋值给低精度(如Int)时会报错。如何优雅地解决这个问题?
1.2 隐式函数基本介绍
隐式转换函数是以implicit关键字声明的带有单个参数的函数。这种函数将会自动应用,将值从一种类型转换为另一种类型。
1.3 隐式函数快速入门
使用隐式函数可以优雅地解决数据类型转换问题:
implicitdeff1(d:Double):Int={d.toInt}// Double 是输入类型,Int 是转换后的类型1.4 隐式函数的底层工作原理
defmain(args:Array[String]):Unit={implicitdeff1(d:Double):Int={d.toInt}implicitdeff2(l:Long):Int={l.toInt}valnum:Int=3.5// 编译器自动调用 f1(3.5)println(num)// 输出: 3valnum2:Int=4.5// 编译器自动调用 f1(4.5)println(num2)// 输出: 3valnum3:Int=20l// 编译器自动调用 f2(20l)}底层实现:编译器在编译时自动查找并调用合适的隐式转换函数。
1.5 隐式转换的注意事项和细节
隐式转换函数的函数名可以是任意的,隐式转换与函数名称无关,只与函数签名(函数参数类型和返回值类型)有关。
隐式函数可以有多个(即隐式函数列表),但是需要保证在当前环境下,只有一个隐式函数能被识别。
// 在当前环境中,不能存在满足条件的多个隐式函数implicitdefa(d:Double)=d.toIntimplicitdefb(d:Double)=d.toIntvali1:Int=3.5// (X) 在转换时,识别出有两个方法可以被使用,// 就不确定调用哪一个,所以出错println(i1)二、隐式转换丰富类库功能
2.1 基本介绍
如果需要为一个类增加一个方法,可以通过隐式转换来实现(动态增加功能)。比如想为MySQL类增加一个delete方法。
2.2 分析解决方案
在当前程序中,如果想要给MySQL类增加功能是非常简单的,但是在实际项目中,如果想要增加新的功能就会需要改变源代码,这是很难接受的。而且违背了软件开发的OCP开发原则(开闭原则 Open Close Principle)。
在这种情况下,可以通过隐式转换函数给类动态添加功能。
2.3 快速入门案例
使用隐式转换方式动态地给MySQL类增加delete方法:
classMySQL{definsert():Unit={println("insert")}}classDB{defdelete():Unit={println("delete")}}// 隐式转换函数implicitdefaddDelete(mysql:MySQL):DB={newDB}valmysql=newMySQL mysql.insert()mysql.delete()// ? 通过隐式转换,MySQL对象可以调用DB的delete方法底层原理:编译器发现MySQL没有delete方法,自动查找隐式转换函数,将MySQL转换为DB类型。
三、隐式值
3.1 基本介绍
隐式值也叫隐式变量,将某个形参变量标记为implicit,所以编译器会在方法省略隐式参数的情况下去搜索作用域内的隐式值作为缺省参数。
3.2 应用案例
implicitvalstr1:String="jack"defhello(implicitname:String):Unit={println(name+" hello")}hello// 调用,不带(),编译器自动传入隐式值str1// 输出: jack hello3.3 课堂测试题
题1:下面的代码是否正确?
objectImplicitVal{defmain(args:Array[String]):Unit={implicitvalstr1:String="jack"// 隐式值defhello(implicitname:String):Unit={// hello$1println(name+" hello")}defhello():Unit={// hello$2println("xxxx")}hello// hello$1(str1) 使用隐式值不要带()}}答案:正确!调用的是带隐式参数的hello方法,传入隐式值"jack"。
题2:
objectScala03{defmain(args:Array[String]):Unit={// 隐式变量(值)implicitvalname:String="Scala"defhello(implicitcontent:String="okook"):Unit={println("Hello "+content)}hello// 输出什么内容?}}答案:输出 “Hello Scala”,隐式值优先级高于默认值。
题3:
objectScala03{defmain(args:Array[String]):Unit={// 隐式变量(值)implicitvalname:Int=10// 类型不匹配!defhello(implicitcontent:String="okook"):Unit={println("Hello "+content)}hello// 输出什么内容?}}答案:输出 “Hello okook”,因为隐式值类型不匹配,使用默认值。
题4:
objectScala03{defmain(args:Array[String]):Unit={// 隐式变量(值)implicitvalname:Int=10// 类型不匹配!defhello(implicitcontent:String):Unit={println("Hello "+content)}hello// 输出什么?}}答案:编译错误!没有匹配的隐式值,也没有默认值。
3.4 多个隐式值的优先级
objectImplicitVal02{defmain(args:Array[String]):Unit={// 隐式变量(值)implicitvalname:String="Scala"implicitvalname1:String="World"defhello(implicitcontent:String="jack"):Unit={println("Hello "+content)}hello// 编译错误!存在多个同类型隐式值,产生歧义}}四、隐式类
4.1 基本介绍
在Scala 2.10后提供了隐式类,可以使用implicit声明类。隐式类非常强大,同样可以扩展类的功能,比前面使用隐式转换丰富类库功能更加的方便,在集合中隐式类会发挥重要的作用。
4.2 隐式类的特点
隐式类使用有如下几个特点:
- 其所带的构造参数有且只能有一个
- 隐式类必须被定义在**"类"或"伴生对象"或"包对象"里,即隐式类不能是顶级的**(top-level objects)
- 隐式类不能是
case class(case class在后续介绍样例类) - 作用域内不能有与之相同名称的标识符
4.3 应用案例
看一个关于隐式类的案例,进一步认识隐式类:
classMySQL1{defsayOk():Unit={println("sayOk")}}defmain(args:Array[String]):Unit={// DB1会对应生成隐式类implicitclassDB1(valm:MySQL1){defaddSuffix():String={m+" scala"}}valmysql1=newMySQL1 mysql1.sayOk()// mysql1.addSuffix() ==> DB1$1(mysql1).addSuffix()// DB1$1(mysql1)返回的类型是 ImplicitClass$DB1$2println(mysql1.addSuffix())}底层原理:编译器自动生成隐式转换函数,将MySQL1对象包装为DB1对象。
五、隐式的转换时机
隐式转换会在以下两种情况下自动触发:
- 当方法中的参数的类型与目标类型不一致时
- 当对象调用所在类中不存在的方法或成员时,编译器会自动将对象进行隐式转换(根据类型)
六、隐式解析机制
即编译器是如何查找到缺失信息的,解析具有以下两种规则:
首先会在当前代码作用域下查找隐式实体(隐式方法、隐式类、隐式对象)。(一般是这种情况)
如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找。类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型T它的查找范围如下:
a) 如果T被定义为
T with A with B with C,那么A,B,C都是T的部分,在T的隐式解析过程中,它们的伴生对象都会被搜索。b) 如果T是参数化类型,那么类型参数和与类型参数相关联的部分都算作T的部分,比如
List[String]的隐式搜索会搜索List的伴生对象和String的伴生对象。c) 如果T是一个单例类型
p.T,即T是属于某个p对象内,那么这个p对象也会被搜索。d) 如果T是个类型注入
S#T,那么S和T都会被搜索。
第二种情况范围广且复杂,在使用时应当尽量避免出现。
七、隐式转换的前提
在进行隐式转换时,需要遵守两个基本的前提:
- 不能存在二义性
- 隐式操作不能嵌套使用
// 使用隐式函数来解决// 说明// 1. 在底层会生成一个函数 f1$1...implicitdeff1(d:Double):Int={d.toInt}valnum3:Int=5.6// 错误!==> f1(5.6)注意:隐式转换不能嵌套,即不能对一个已经经过隐式转换的值再次进行隐式转换。
总结
本章深入讲解了Scala隐式机制的四大核心内容:
| 特性 | 核心要点 |
|---|---|
| 隐式转换 | implicit def,单参数,自动类型转换 |
| 隐式值 | implicit val,作为方法缺省参数 |
| 隐式类 | implicit class,构造参数只能有一个,不能是顶级 |
| 隐式解析 | 当前作用域优先,类型作用域补充 |
隐式机制是Scala最强大的特性之一,合理使用可以大大简化代码,但过度使用会降低代码可读性。建议在类型增强、DSL构建、类型类模式等场景下使用。