Scala implicit 隐式转换安全驾驶指南
这篇短文将结合实例对隐式转换的各种场景进行解释和总结,希望看完的人能够安全驶过隐式转换这个大坑。
隐式转换函数
隐式转换函数有两种作用场景。
- 1 转换为期望类型:就是指一旦编译器看到X,但需要Y,就会检查从X到Y的隐式转换函数。
- 2 转换方法的调用者:简单来说,如obj.f(),如果obj对象没有f方法,则尝试将obj转换为拥有f方法的类型。
object ImpFunction extends App { class Dog(val name: String) { def bark(): Unit = println(s"$name say: Wang !") } implicit def double2int(d: Double): Int = d.toInt implicit def string2Dog(s: String): Dog = new Dog(s) val f: Int = 1.1 //转换为期望类型,1.1通过double2int转成了Int类型 println(f) "Teddy".bark() // 转换方法的调用者,字符串通过string2Dog转成了Dog, 于是有了bark方法 } // output // 1 // Teddy say: Wang !
val f: Int = 1.1
因为类型不匹配,这段本来是无法通过编译的,但是编译器发现存在一个Double至Int的隐式转换函数,所以进行了隐式转换。
"Teddy".bark()
String类型本来是没有bark方法的,但是编译器发现了隐式转换string2Dog可以使得String转成一种拥有bark方法的类型,相当于进行了这样的转换:string2Dog("Teddy").bark()
。
注意事项
需要注意的是,编译器只关心隐式转换函数的输入输出类型,不关心函数名,为避免歧义,同一个作用域中不能有输入输出类型相同的两个隐式转换函数,不然编译器会报错。
隐式类
Scala 2.10引入了一种叫做隐式类的新特性。隐式类指的是用implicit关键字修饰的类。使用情况与隐式转换函数类似,可以看做将类的构造函数定义为隐式转换函数,返回类型就是这个类。
package io.github.liam8.impl object ImpClass extends App { implicit class Dog(val name: String) { def bark(): Unit = println(s"$name say: Wang !") } "Teddy".bark() }
注意事项
这段来自官网IMPLICIT CLASSES 隐式类有以下限制条件:
- 1 只能在别的trait/类/对象内部定义。
object Helpers { implicit class RichInt(x: Int) // 正确! } implicit class RichDouble(x: Double) // 错误!
- 2 构造函数只能携带一个非隐式参数。
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) // 正确!
虽然我们可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。
- 3 在同一作用域内,不能有任何方法、成员或对象与隐式类同名。 注意:这意味着隐式类不能是case class。
object Bar implicit class Bar(x: Int) // 错误! val x = 5 implicit class x(y: Int) // 错误! implicit case class Baz(x: Int) // 错误!
隐式参数 & 隐式值
package io.github.liam8.impl object ImpParam extends App { def bark(implicit name: String): Unit = println(s"$name say: Wang !") implicit val t: String = "Hot Dog" bark }
参数加上implicit就成了隐式参数,需要与隐式值(变量定义加上implicit)搭配使用,最后一行的bark
缺少了一个String类型的参数,编译器找到了String类型的隐式值,便将其传入,相当于执行了bark(t)
。
implicit关键字会作用于函数列表中的的所有参数,如def test(implicit x:Int, y: Double)
这样定义函数,x和y就都成了隐式函数。但是通常我们只希望部分参数为隐式参数,就好比通常会给部分参数提供默认值而不是全部都指定默认值,于是隐式参数常常与柯里化函数一起使用,这样可以使得只有最后一个参数为隐式参数,例如def test(x: Int)(implicit y: Double)
。
👇是完整的例子。
object ImpParamWithCurry extends App { def bark(name: String)(implicit word: String): Unit = println(s"$name say: $word !") implicit val w: String = "Wang" bark("Hot Dog") }
注意事项
下面这段来自scala的隐式转换学习总结(详细)
- 1)当函数没有柯里化时,implicit关键字会作用于函数列表中的的所有参数。
- 2)隐式参数使用时要么全部不指定,要么全不指定,不能只指定部分。
- 3)同类型的隐式值只能在作用域内出现一次,即不能在同一个作用域中定义多个相同类型的隐式值。
- 4)在指定隐式参数时,implicit 关键字只能出现在参数开头。
- 5)如果想要实现参数的部分隐式参数,只能使用函数的柯里化, 如要实现这种形式的函数,def test(x:Int, implicit y: Double)的形式,必须使用柯里化实现:def test(x: Int)(implicit y: Double).
- 6)柯里化的函数, implicit 关键字只能作用于最后一个参数。否则,不合法。
- 7)implicit 关键字在隐式参数中只能出现一次,柯里化的函数也不例外!
隐式对象
类似于隐式值, 要结合隐式参数使用。先看一个栗子(下面的代码需要认真体会)。
package io.github.liam8.impl object ImpObject extends App { //定义一个`排序器`接口,能够比较两个相同类型的值的大小 trait Ordering[T] { //如果x<y返回-1,x>y返回1,x==y则返回0. def compare(x: T, y: T): Int } //实现一个Int类型的排序器 implicit object IntOrdering extends Ordering[Int] { override def compare(x: Int, y: Int): Int = { if (x < y) -1 else if (x == y) 0 else 1 } } //实现一个String类型的排序器 implicit object StringOrdering extends Ordering[String] { override def compare(x: String, y: String): Int = x.compareTo(y) } //一个通用的max函数 def max[T](x: T, y: T)(implicit ord: Ordering[T]): T = { if (ord.compare(x, y) >= 0) x else y } println(max(1, 2)) println(max("a", "b")) } //output: // 2 // b
max函数的作用显然是返回x和y中的最大值,但是x和y的值类型不是固定的,max不知道如何比较x和y的大型,于是定义了一个隐式参数implicit ord: Ordering[T]
,希望能传入一个Ordering[T]类型的排序器帮助进行x和y的比较。
在调用max(1, 2)
的时候,编译器发现需要一个Ordering[Int]类型的参数,刚好implicit object IntOrdering
定义了一个隐式对象符合要求,于是被用来传入max函数。
隐式对象跟上面的隐式值非常相似,只是类型特殊而已。
在Scala中scala.math.Ordering很常用的内置特质,如果你理解了这段代码,也就大致理解了Ordering的原理。
上下文界定(context bounds)
这是一种隐式参数的语法糖。
再看上面隐式对象的例子,如果要添加一个min函数,大致就是这样
def min[T](x: T, y: T)(implicit ord: Ordering[T]): T = { if (ord.compare(x, y) >= 0) y else x }
但是max和min函数的参数都比较长,于是出现了一种简化的写法
def min[T: Ordering](x: T, y: T): T = { val ord = implicitly[Ordering[T]] if (ord.compare(x, y) >= 0) y else x }
[T: Ordering]
这种语法就叫上下文界定,含义是上下文中必须有一个Ordering[T]类型的隐式值,这个值会被传入min函数。但是由于这个隐式值并没有明确赋值给某个变量,没法直接使用它,所以需要一个implicitly函数把隐式值取出来。
implicitly函数的定义非常简单,作用就是将T类型的隐含值返回:
@inline def implicitly[T](implicit e: T) = e
视界
这个语法已经被废弃了,但是你还是可能会看到,简单解释下。
def min[T <% Ordered[T]](x: T, y: T): T = { if (x > y) y else x }
视界的定义T <% Ordered[T]
的含义是T可以被隐式转换成Ordered[T],这也是为什么x > y
可以编译通过。
上面的写法其实等同于下面这样,所以视界的语法不能用了也不要紧。
def min[T](x: T, y: T)(implicit c: T => Ordered[T]): T = { if (x > y) y else x }
隐式转换机制
隐式转换通用规则
-
标记规则:只有标记为implicit的定义才是可用的。
-
作用域规则:插入的隐式转换必须以单一标识符的形式处于作用域中,或与转换的源或目标类型关联在一起。
单一标识符意思是不能插入形式为someVariable.convert(x)的转换,只能是convert(x)。 单一标识符规则有个例外,编译器还将在源类型或转换的期望目标类型的伴生对象中寻找隐式定义。
有点难理解?看个例子!
package io.github.liam8.impl object ImpCompObject extends App { object Dog { implicit def dogToCat(d: Dog) = new Cat(d.name) } class Cat(val name: String) { def miao(): Unit = println(s"$name say: Miao !") } class Dog(val name: String) { def bark(): Unit = println(s"$name say: Wang !") } new Dog("Teddy").miao() } //Teddy say: Miao !
当前作用域中没有定义和引入隐式函数,但是在Dog的伴生对象中找到了,所以Dog可以被转成Cat,这个跟上下文没有关系,而是Dog自带技能。
-
无歧义规则:隐式转换唯有不存在其他转换的前提下有效。
-
单一调用规则:只会尝试一个隐式操作。
-
显示操作先行规则:若编写的代码类型检查无误,则不会尝试隐式操作。
转换时机
- 当类型与目标类型不一致时
- 当对象调用类中不存在的方法或成员时
- 缺少隐式参数时
也即是能用到隐式操作的有三个地方:转换为期望类型、指定(方法)调用者的转换、隐式参数。
转换机制
这段来自深入理解Scala的隐式转换
即编译器是如何查找到缺失信息的,解析具有以下两种规则:
-
1.首先会在当前代码作用域下查找隐式实体(隐式方法 隐式类 隐式对象)
-
2.如果第一条规则查找隐式实体失败,会继续在隐式参数的类型的作用域里查找 类型的作用域是指与该类型相关联的全部伴生模块,一个隐式实体的类型T它的查找范围如下:
- 1 如果T被定义为T with A with B with C,那么A,B,C都是T的部分,在T的隐式解析过程中,它们的伴生对象都会被搜索
- 2 如果T是参数化类型,那么类型参数和与类型参数相关联的部分都算作T的部分,比如List[String]的隐式搜索会搜索List的伴生对象和String的伴生对象
- 3 如果T是一个单例类型p.T,即T是属于某个p对象内,那么这个p对象也会被搜索
- 4 如果T是个类型注入S#T,那么S和T都会被搜索
上路前的话
这段话来自《Scala编程》
隐式操作若过于频繁使用,会让代码变得晦涩难懂。因此,在考虑添加新的隐式转换之前,请首先自问是否能够通过其他手段,诸如继承、混入组合或方法重载,达到同样的目的。如果所有这些都不能成功,并且你感觉代码仍有一些繁复和冗余,那么隐式操作或许正好能帮到你。
所以。。。谨慎使用,小心翻车,good luck!
参考文献
《Scala编程》
本文代码
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
ES2019 中的 JavaScript 新特性
JavaScript 从成立之初就已经走了很长一段路,提供了许多新的功能,这些功能是专门设计来使该语言更加人性化和提升效率。以下是我最近发现的一些有趣的JavaScript 新增内容。其中一些功能已在 Node,Chrome,Firefox 和 Safari 中可用,而其他功能仍处于建议阶段。 Optional chaining (可选链) Optional chaining 可选链使用?.操作符来表示,Optional Chaining 使我们能检查一个对象上面是否存在某属性。其它一些语言有类似的特性。例如 C# 有 Null Conditional 操作符。而 JavaScript 的 Optional chaining 在需要深度访问嵌套对象属性时就特别有用。 当我们在访问对象的属性前,我们需要用?.操作符来确定该属性是否存在。 首先看看下面代码: const users = [ { name: "Olagunju Gbolahan", occupation: "Software Developer", sayName(){ console.log(`my name i...
- 下一篇
操作系统学习(一)-- 从发展史理解操作系统设计需求
这是操作系统系列第 1 篇。 尽管操作系统发展史不是研究操作系统的重点,但是在这一发展过程中,衍生出了许许多多与操作系统相关的重要概念,如果知道这些概念出现在怎样的背景下,以及产生的原因,在后期学习中就不会觉得一些概念出现的比较突兀。除此之外,了解操作系统的发展史,理解设计需求,有助于我们站在计算机的角度思考问题。 ENIAC 与串行处理 计算机的发展可以追溯到 1946 年,世界上第一台通用计算机「ENIAC」 诞生在这一年的 2 月14 日(这天正好是情人节)。 图为 ENIAC > ENIAC 长 30.48 米,宽 6 米,高 2.4 米,占地面积约 170 平方米,30 个操作台,重达 30 英吨,耗电量 150 千瓦,造价 48 万美元。 从这一年一直到 20 世纪 50 年代中期,操作系统是不存在的。毕竟那时候还没有操作系统这个概念。程序员要是想要运行什么程序,得把机器代码用打孔机打在纸带上(这不仅是个智力活,还是个要细心的活儿,打错一个孔你就得重来。想象一下,让你写一篇文章,不用退格键,更不能从中间插入。。。当时的程序员太难了),然后通过输入设备,比如纸带阅读机,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Mario游戏-低调大师作品
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Red5直播服务器,属于Java语言的直播服务器
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,CentOS7官方镜像安装Oracle11G