一、隐式转换

1.1 使用隐式转换

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

// 普通人
class Person(val name: String)

// 雷神
class Thor(val name: String) {
  // 正常情况下只有雷神才能举起雷神之锤
  def hammer(): Unit = {
    println(name + "举起雷神之锤")
  }
}

object Thor extends App {
  // 定义隐式转换方法 将普通人转换为雷神 通常建议方法名使用source2Target,即:被转换对象To转换对象
  implicit def person2Thor(p: Person): Thor = new Thor(p.name)
  // 这样普通人也能举起雷神之锤
  new Person("普通人").hammer()
}

输出: 普通人举起雷神之锤

1.2 隐式转换规则

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

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

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

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

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

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

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

class ClassA {
  override def toString = "This is Class A"
}

class ClassB {
  override def toString = "This is Class B"
  def printB(b: ClassB): Unit = println(b)
}

class ClassC

class ClassD

object ImplicitTest extends App {
  implicit def A2B(a: ClassA): ClassB = {
    println("A2B")
    new ClassB
  }

  implicit def C2B(c: ClassC): ClassB = {
    println("C2B")
    new ClassB
  }

  implicit def D2C(d: ClassD): ClassC = {
    println("D2C")
    new ClassC
  }

  // 这行代码无法通过编译,因为要调用到printB方法,需要执行两次转换C2B(D2C(ClassD))
  new ClassD().printB(new ClassA)

  /*
   *  下面的这一行代码虽然也进行了两次隐式转换,但是两次的转换对象并不是一个对象,所以它是生效的:
   *  转换流程如下:
   *  1. ClassC中并没有printB方法,因此隐式转换为ClassB,然后调用printB方法;
   *  2. 但是printB参数类型为ClassB,然而传入的参数类型是ClassA,所以需要将参数ClassA转换为ClassB,这是第二次;
   *  即: C2B(ClassC) -> ClassB.printB(ClassA) -> ClassB.printB(A2B(ClassA)) -> ClassB.printB(ClassB)
   *  转换过程1的对象是ClassC,而转换过程2的转换对象是ClassA,所以虽然是一行代码两次转换,但是仍然是有效转换
   */
  new ClassC().printB(new ClassA)
}

// 输出:
C2B
A2B
This is Class B

1.3 引入隐式转换

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

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

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

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

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

定义在一个公共的对象中

object Convert {
  implicit def person2Thor(p: Person): Thor = new Thor(p.name)
}
// 导入Convert下所有的隐式转换函数
import com.heibaiying.Convert._

object ScalaApp extends App {
  new Person("普通人").hammer()
}

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

二、隐式参数

2.1 使用隐式参数

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

// 定义分隔符类
class Delimiters(val left: String, val right: String)

object ScalaApp extends App {

    // 进行格式化输出
  def formatted(context: String)(implicit deli: Delimiters): Unit = {
    println(deli.left + context + deli.right)
  }

  // 定义一个隐式默认值 使用左右中括号作为分隔符
  implicit val bracket = new Delimiters("(", ")")
  formatted("this is context") // 输出: (this is context)
}

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

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

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

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

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

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

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

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

2.2 引入隐式参数

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

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

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

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

class Delimiters(val left: String, val right: String)

object Delimiters {
  implicit val bracket = new Delimiters("(", ")")
}
// 此时执行代码的上下文中不用定义
object ScalaApp extends App {

  def formatted(context: String)(implicit deli: Delimiters): Unit = {
    println(deli.left + context + deli.right)
  }
  formatted("this is context")
}

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

object Convert {
  implicit val bracket = new Delimiters("(", ")")
}
// 在使用的时候导入
import com.heibaiying.Convert.bracket

object ScalaApp extends App {
  def formatted(context: String)(implicit deli: Delimiters): Unit = {
    println(deli.left + context + deli.right)
  }
  formatted("this is context") // 输出: (this is context)
}

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

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

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

trait Ordered[A] extends Any with java.lang.Comparable[A] {
  def compare(that: A): Int
  def <  (that: A): Boolean = (this compare that) <  0
  def >  (that: A): Boolean = (this compare that) >  0
  def <= (that: A): Boolean = (this compare that) <= 0
  def >= (that: A): Boolean = (this compare that) >= 0
  def compareTo(that: A): Int = compare(that)
}

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

1. 使用视图界定

object Pair extends App {

 // 视图界定
  def smaller[T<% Ordered[T]](a: T, b: T) = if (a < b) a else b

  println(smaller(1,2)) //输出 1
}

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

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

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

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

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

object Pair extends App {

   // order既是一个隐式参数也是一个隐式转换,即如果a不存在 < 方法,则转换为order(a)<b
  def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b

  println(smaller(1,2)) //输出 1
}

参考资料

  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. 写在使用 Linux 工作一年后

    start 去年公司空了几台台式机,当时看了下似乎配置比我用的乞丐版 air 略高一些,而且除了 ssd 以外还有一个 1T 的大硬盘,加上后面可能会有一段时间不做 iOS 了,那就不需要 macOS ...

  2. 使用 Microsoft.UI.Xaml 解决 UWP 控件和对老版本 Windows 10 的兼容性问题

    原文 使用 Microsoft.UI.Xaml 解决 UWP 控件和对老版本 Windows 10 的兼容性问题 虽然微软宣称 Windows 10 将是最后一个 Windows 版本,但由于年代跨越 ...

  3. OpenGL(十一) BMP真彩文件的显示和复制操作

    glut窗口除了可以绘制矢量图之外,还可以显示BMP文件,用函数glDrawPixels把内存块中的图像数据绘制到窗口上,glDrawPixels函数原型: glDrawPixels (GLsizei ...

  4. POJ - 2991 Crane (段树+计算几何)

    Description ACM has bought a new crane (crane -- jeřáb) . The crane consists of n segments of variou ...

  5. WPF ListBox的内容属性Items

    <Window x:Class="XamlTest.Window3"        xmlns="http://schemas.microsoft.com/winf ...

  6. Ackerman 函数

    先留个简介: 函数定义: 从定义可以看出是一个递归函数.阿克曼函数不仅值增长的非常快,而且递归深度很高. 一般用来测试编译其优化递归调用的能力.. 如果用一下代码简单实现的话,输入参数4,2程序就直接 ...

  7. n阶贝塞尔曲线绘制(C/C#)

    原文:n阶贝塞尔曲线绘制(C/C#) 贝塞尔是很经典的东西,轮子应该有很多的.求n阶贝塞尔曲线用到了 德卡斯特里奥算法(De Casteljau's Algorithm) 需要拷贝代码请直接使用本文最 ...

  8. 字符串、数组操作函数 Copy Concat Delete Insert High MidStr Pos SetLength StrPCopy TrimLeft

    对字符串及数组的操作,是每个程序员必须要掌握的.熟练的使用这些函数,在编程时能更加得心应手. 1.Copy 功能说明:该函数用于从字符串中复制指定范围中的字符.该函数有3个参数.第一个参数是数据源(即 ...

  9. PHP 实现自动加载器(Autoloader)

    我们知道PHP可以实现自动加载,避免了繁重的体力活,代码更规范,整洁.那如果我们把这个自动加载再升华一下,变成自动加载类,每次只需要引入这个类,那么其他类就自动加载了,已经开源,仓库地址在这里.同时如 ...

  10. 微信小程序把玩(二十八)image组件

    原文:微信小程序把玩(二十八)image组件 image组件也是一个程序不可缺少的,可以这样说一个app中image组件随处可以看到,一般 image有两种加载方式第一种是网络图片第二种是本地图片资源 ...