Scala 系列(十三)—— 隐式转换和隐式参数

1、隐式转换

1.1 使用隐式转换

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

// 普通人
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() 方法的时候,普通人就仍是普通人。一般程序会在如下状况下尝试执行隐式转换:git

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

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

  • 若是代码可以在不使用隐式转换的前提下经过编译,则不会使用隐式转换;
  • 编译器不会尝试同时执行多个转换,好比 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()
复制代码

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

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 引入隐式转换

隐式转换的能够定义在如下三个地方:app

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

上面咱们使用的方法至关于直接定义在执行代码的做用域中,下面分别给出其余两种定义的代码示例:ide

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

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 查看所有隐式转换函数。this


2、隐式参数

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

https://github.com/heibaiying

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 开源项目大数据入门指南

相关文章
相关标签/搜索