一、隐式转换

1.1 使用隐式转换

隐式转换指的是以implicit关键字声明带有单个参数的转换函数,它将值从一种类型转换为另一种类型,以便使用之前类型所没有的功能。示例如下:

  1. // 普通人
  2. class Person(val name: String)
  3. // 雷神
  4. class Thor(val name: String) {
  5. // 正常情况下只有雷神才能举起雷神之锤
  6. def hammer(): Unit = {
  7. println(name + "举起雷神之锤")
  8. }
  9. }
  10. object Thor extends App {
  11. // 定义隐式转换方法 将普通人转换为雷神 通常建议方法名使用source2Target,即:被转换对象To转换对象
  12. implicit def person2Thor(p: Person): Thor = new Thor(p.name)
  13. // 这样普通人也能举起雷神之锤
  14. new Person("普通人").hammer()
  15. }
  16. 输出: 普通人举起雷神之锤

1.2 隐式转换规则

并不是你使用implicit转换后,隐式转换就一定会发生,比如上面如果不调用hammer()方法的时候,普通人就还是普通人。通常程序会在以下情况下尝试执行隐式转换:

  • 当对象访问一个不存在的成员时,即调用的方法不存在或者访问的成员变量不存在;
  • 当对象调用某个方法,该方法存在,但是方法的声明参数与传入参数不匹配时。

而在以下三种情况下编译器不会尝试执行隐式转换:

  • 如果代码能够在不使用隐式转换的前提下通过编译,则不会使用隐式转换;
  • 编译器不会尝试同时执行多个转换,比如convert1(convert2(a))*b
  • 转换存在二义性,也不会发生转换。

这里首先解释一下二义性,上面的代码进行如下修改,由于两个隐式转换都是生效的,所以就存在了二义性:

  1. //两个隐式转换都是有效的
  2. implicit def person2Thor(p: Person): Thor = new Thor(p.name)
  3. implicit def person2Thor2(p: Person): Thor = new Thor(p.name)
  4. // 此时下面这段语句无法通过编译
  5. new Person("普通人").hammer()

其次再解释一下多个转换的问题:

  1. class ClassA {
  2. override def toString = "This is Class A"
  3. }
  4. class ClassB {
  5. override def toString = "This is Class B"
  6. def printB(b: ClassB): Unit = println(b)
  7. }
  8. class ClassC
  9. class ClassD
  10. object ImplicitTest extends App {
  11. implicit def A2B(a: ClassA): ClassB = {
  12. println("A2B")
  13. new ClassB
  14. }
  15. implicit def C2B(c: ClassC): ClassB = {
  16. println("C2B")
  17. new ClassB
  18. }
  19. implicit def D2C(d: ClassD): ClassC = {
  20. println("D2C")
  21. new ClassC
  22. }
  23. // 这行代码无法通过编译,因为要调用到printB方法,需要执行两次转换C2B(D2C(ClassD))
  24. new ClassD().printB(new ClassA)
  25. /*
  26. * 下面的这一行代码虽然也进行了两次隐式转换,但是两次的转换对象并不是一个对象,所以它是生效的:
  27. * 转换流程如下:
  28. * 1. ClassC中并没有printB方法,因此隐式转换为ClassB,然后调用printB方法;
  29. * 2. 但是printB参数类型为ClassB,然而传入的参数类型是ClassA,所以需要将参数ClassA转换为ClassB,这是第二次;
  30. * 即: C2B(ClassC) -> ClassB.printB(ClassA) -> ClassB.printB(A2B(ClassA)) -> ClassB.printB(ClassB)
  31. * 转换过程1的对象是ClassC,而转换过程2的转换对象是ClassA,所以虽然是一行代码两次转换,但是仍然是有效转换
  32. */
  33. new ClassC().printB(new ClassA)
  34. }
  35. // 输出:
  36. C2B
  37. A2B
  38. This is Class B

1.3 引入隐式转换

隐式转换的可以定义在以下三个地方:

  • 定义在原类型的伴生对象中;
  • 直接定义在执行代码的上下文作用域中;
  • 统一定义在一个文件中,在使用时候导入。

上面我们使用的方法相当于直接定义在执行代码的作用域中,下面分别给出其他两种定义的代码示例:

定义在原类型的伴生对象中

  1. class Person(val name: String)
  2. // 在伴生对象中定义隐式转换函数
  3. object Person{
  4. implicit def person2Thor(p: Person): Thor = new Thor(p.name)
  5. }
  1. class Thor(val name: String) {
  2. def hammer(): Unit = {
  3. println(name + "举起雷神之锤")
  4. }
  5. }
  1. // 使用示例
  2. object ScalaApp extends App {
  3. new Person("普通人").hammer()
  4. }

定义在一个公共的对象中

  1. object Convert {
  2. implicit def person2Thor(p: Person): Thor = new Thor(p.name)
  3. }
  1. // 导入Convert下所有的隐式转换函数
  2. import com.heibaiying.Convert._
  3. object ScalaApp extends App {
  4. new Person("普通人").hammer()
  5. }

注:Scala自身的隐式转换函数大部分定义在Predef.scala中,你可以打开源文件查看,也可以在Scala交互式命令行中采用:implicit -v查看全部隐式转换函数。

二、隐式参数

2.1 使用隐式参数

在定义函数或方法时可以使用标记为implicit的参数,这种情况下,编译器将会查找默认值,提供给函数调用。

  1. // 定义分隔符类
  2. class Delimiters(val left: String, val right: String)
  3. object ScalaApp extends App {
  4. // 进行格式化输出
  5. def formatted(context: String)(implicit deli: Delimiters): Unit = {
  6. println(deli.left + context + deli.right)
  7. }
  8. // 定义一个隐式默认值 使用左右中括号作为分隔符
  9. implicit val bracket = new Delimiters("(", ")")
  10. formatted("this is context") // 输出: (this is context)
  11. }

关于隐式参数,有两点需要注意:

1.我们上面定义formatted函数的时候使用了柯里化,如果你不使用柯里化表达式,按照通常习惯只有下面两种写法:

  1. // 这种写法没有语法错误,但是无法通过编译
  2. def formatted(implicit context: String, deli: Delimiters): Unit = {
  3. println(deli.left + context + deli.right)
  4. }
  5. // 不存在这种写法,IDEA直接会直接提示语法错误
  6. def formatted( context: String, implicit deli: Delimiters): Unit = {
  7. println(deli.left + context + deli.right)
  8. }

上面第一种写法编译的时候会出现下面所示error信息,从中也可以看出implicit是作用于参数列表中每个参数的,这显然不是我们想要到达的效果,所以上面的写法采用了柯里化。

  1. not enough arguments for method formatted:
  2. (implicit context: String, implicit deli: com.heibaiying.Delimiters)

2.第二个问题和隐式函数一样,隐式默认值不能存在二义性,否则无法通过编译,示例如下:

  1. implicit val bracket = new Delimiters("(", ")")
  2. implicit val brace = new Delimiters("{", "}")
  3. formatted("this is context")

上面代码无法通过编译,出现错误提示ambiguous implicit values,即隐式值存在冲突。

2.2 引入隐式参数

引入隐式参数和引入隐式转换函数方法是一样的,有以下三种方式:

  • 定义在隐式参数对应类的伴生对象中;
  • 直接定义在执行代码的上下文作用域中;
  • 统一定义在一个文件中,在使用时候导入。

我们上面示例程序相当于直接定义执行代码的上下文作用域中,下面给出其他两种方式的示例:

定义在隐式参数对应类的伴生对象中

  1. class Delimiters(val left: String, val right: String)
  2. object Delimiters {
  3. implicit val bracket = new Delimiters("(", ")")
  4. }
  1. // 此时执行代码的上下文中不用定义
  2. object ScalaApp extends App {
  3. def formatted(context: String)(implicit deli: Delimiters): Unit = {
  4. println(deli.left + context + deli.right)
  5. }
  6. formatted("this is context")
  7. }

统一定义在一个文件中,在使用时候导入

  1. object Convert {
  2. implicit val bracket = new Delimiters("(", ")")
  3. }
  1. // 在使用的时候导入
  2. import com.heibaiying.Convert.bracket
  3. object ScalaApp extends App {
  4. def formatted(context: String)(implicit deli: Delimiters): Unit = {
  5. println(deli.left + context + deli.right)
  6. }
  7. formatted("this is context") // 输出: (this is context)
  8. }

2.3 利用隐式参数进行隐式转换

  1. def smaller[T] (a: T, b: T) = if (a < b) a else b

在Scala中如果定义了一个如上所示的比较对象大小的泛型方法,你会发现无法通过编译。对于对象之间进行大小比较,Scala和Java一样,都要求被比较的对象需要实现java.lang.Comparable接口。在Scala中,直接继承Java中Comparable接口的是特质Ordered,它在继承compareTo方法的基础上,额外定义了关系符方法,源码如下:

  1. trait Ordered[A] extends Any with java.lang.Comparable[A] {
  2. def compare(that: A): Int
  3. def < (that: A): Boolean = (this compare that) < 0
  4. def > (that: A): Boolean = (this compare that) > 0
  5. def <= (that: A): Boolean = (this compare that) <= 0
  6. def >= (that: A): Boolean = (this compare that) >= 0
  7. def compareTo(that: A): Int = compare(that)
  8. }

所以要想在泛型中解决这个问题,有两种方法:

1. 使用视图界定

  1. object Pair extends App {
  2. // 视图界定
  3. def smaller[T<% Ordered[T]](a: T, b: T) = if (a < b) a else b
  4. println(smaller(1,2)) //输出 1
  5. }

视图限定限制了T可以通过隐式转换Ordered[T],即对象一定可以进行大小比较。在上面的代码中smaller(1,2)中参数12实际上是通过定义在Predef中的隐式转换方法intWrapper转换为RichInt

  1. // Predef.scala
  2. @inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)

为什么要这么麻烦执行隐式转换,原因是Scala中的Int类型并不能直接进行比较,因为其没有实现Ordered特质,真正实现Ordered特质的是RichInt

2. 利用隐式参数进行隐式转换

Scala2.11+后,视图界定被标识为废弃,官方推荐使用类型限定来解决上面的问题,本质上就是使用隐式参数进行隐式转换。

  1. object Pair extends App {
  2. // order既是一个隐式参数也是一个隐式转换,即如果a不存在 < 方法,则转换为order(a)<b
  3. def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b
  4. println(smaller(1,2)) //输出 1
  5. }

参考资料

  1. Martin Odersky . Scala编程(第3版)[M] . 电子工业出版社 . 2018-1-1
  2. 凯.S.霍斯特曼 . 快学Scala(第2版)[M] . 电子工业出版社 . 2017-7

更多大数据系列文章可以参见个人 GitHub 开源项目: 程序员大数据入门指南

Scala 学习之路(十三)—— 隐式转换和隐式参数的更多相关文章

  1. Scala学习之路 (八)Scala的隐式转换和隐式参数

    一.概念 Scala 2.10引入了一种叫做隐式类的新特性.隐式类指的是用implicit关键字修饰的类.在对应的作用域内,带有这个关键字的类的主构造函数可用于隐式转换. 隐式转换和隐式参数是Scal ...

  2. Spark基础-scala学习(八、隐式转换与隐式参数)

    大纲 隐式转换 使用隐式转换加强现有类型 导入隐式转换函数 隐式转换的发生时机 隐式参数 隐式转换 要实现隐式转换,只要程序可见的范围内定义隐式转换函数即可.Scala会自动使用隐式转换函数.隐式转换 ...

  3. 大数据技术之_16_Scala学习_06_面向对象编程-高级+隐式转换和隐式值

    第八章 面向对象编程-高级8.1 静态属性和静态方法8.1.1 静态属性-提出问题8.1.2 基本介绍8.1.3 伴生对象的快速入门8.1.4 伴生对象的小结8.1.5 最佳实践-使用伴生对象解决小孩 ...

  4. Scala 中的隐式转换和隐式参数

    隐式定义是指编译器为了修正类型错误而允许插入到程序中的定义. 举例: 正常情况下"120"/12显然会报错,因为 String 类并没有实现 / 这个方法,我们无法去决定 Stri ...

  5. Scala隐式转换和隐式参数

    隐式转换 Scala提供的隐式转换和隐式参数功能,是非常有特色的功能.是Java等编程语言所没有的功能.它可以允许你手动指定,将某种类型的对象转换成其他类型的对象或者是给一个类增加方法.通过这些功能, ...

  6. Scala基础:闭包、柯里化、隐式转换和隐式参数

    闭包,和js中的闭包一样,返回值依赖于声明在函数外部的一个或多个变量,那么这个函数就是闭包函数. val i: Int = 20 //函数func的方法体中使用了在func外部定义的变量 那func就 ...

  7. Qt 学习之路 2(40):隐式数据共享

    Qt 学习之路 2(40):隐式数据共享 豆子 2013年1月21日 Qt 学习之路 2 14条评论 Qt 中许多 C++ 类使用了隐式数据共享技术,来最大化资源利用率和最小化拷贝时的资源消耗.当作为 ...

  8. 12、scala隐式转换与隐式参数

    一.隐式转换 1.介绍 Scala提供的隐式转换和隐式参数功能,是非常有特色的功能.是Java等编程语言所没有的功能.它可以允许你手动指定,将某种类型的对象转换成其他类型的对象. 通过这些功能,可以实 ...

  9. Scala入门到精通——第十九节 隐式转换与隐式參数(二)

    作者:摇摆少年梦 配套视频地址:http://www.xuetuwuyou.com/course/12 本节主要内容 隐式參数中的隐式转换 函数中隐式參数使用概要 隐式转换问题梳理 1. 隐式參数中的 ...

  10. 02.Scala高级特性:第6节 高阶函数;第7节 隐式转换和隐式参数

    Scala高级特性 1.    课程目标 1.1.   目标一:深入理解高阶函数 1.2.   目标二:深入理解隐式转换 2.    高阶函数 2.1.   概念 Scala混合了面向对象和函数式的特 ...

随机推荐

  1. 配置mysql 及 设置密码

    https://jingyan.baidu.com/article/8cdccae946133f315513cd6a.html

  2. Cocos2dx 温馨提示(十)三种缓存类演示

    在介绍了三级缓存,首先,我们必须弄清楚的一个问题:什么是纹理?它的通俗的解释,质感是它的含义的图片. 当图像加载到内存中后,,它是存在于纹理的形式.由于从这里可以看出.纹理事实上就是一块内存.这块内存 ...

  3. mybatis如何实现分页功能?

    1)原始方法,使用limit,需要自己处理分页逻辑: 对于mysql数据库可以使用limit,如: select * from table limit 5,10; --返回6-15行 对于oracle ...

  4. ADO.net Connection对象简介

    Connection对象 学习的是刘皓的文章  ADO.NET入门教程(四) 品味Connection对象 这篇文章开始水平一般起来了,主要介绍了要优雅的使用这个对象 1 用try...catch.. ...

  5. Leetcode 122 Best Time to Buy and Sell Stock II 贪心

    用一个数组表示股票每天的价格,数组的第i个数表示股票在第i天的价格.交易次数不限,但一次只能交易一支股票,也就是说手上最多只能持有一支股票,求最大收益. 关键:能赚就赚 class Solution ...

  6. 在WPF设计工具Blend2中制作立方体图片效果

    原文:在WPF设计工具Blend2中制作立方体图片效果 ------------------------------------------------------------------------ ...

  7. uboot初体验-----趣谈nand设备发起的浅显理解

    1 选择Uboot版本号 2 移植uboot至console正常work 3 制造uImage和使用uboot指南 4 写NFC驱动器 5 uboot从nand启动引导系统 1 选择Uboot版本号 ...

  8. 使用 NodeJS + Express从GET/POST Request 取值

    过去无论哪一种网站应用程式的开发语言,初学者教学中第一次会提到的起手式,八九不离十就是GET/POST Request 的取值.但是,在Node.js + Express 的世界中,仿佛人人是高手,天 ...

  9. WPF 视图导航

    <Window x:Class="ViewExam.MainWindow"        xmlns="http://schemas.microsoft.com/w ...

  10. 什么是DirectShow?

    DirectShow是微软出的用于流媒体开发的开发包.开发语言是C++,没提供C#接口的调用方式.