Scala implicit 隐式转换安全驾驶指南

2019-10-28  本文已影响0人  Liam666

这篇短文将结合实例对隐式转换的各种场景进行解释和总结,希望看完的人能够安全驶过隐式转换这个大坑。

隐式转换函数

隐式转换函数有两种作用场景。

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
隐式类有以下限制条件:

    object Helpers {
       implicit class RichInt(x: Int) // 正确!
    }
    implicit class RichDouble(x: Double) // 错误!
    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) // 正确!

虽然我们可以创建带有多个非隐式参数的隐式类,但这些类无法用于隐式转换。

    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的隐式转换学习总结(详细)

隐式对象

类似于隐式值, 要结合隐式参数使用。先看一个栗子(下面的代码需要认真体会)。

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
  }

隐式转换机制

隐式转换通用规则

单一标识符意思是不能插入形式为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的隐式转换

即编译器是如何查找到缺失信息的,解析具有以下两种规则:

上路前的话

这段话来自《Scala编程》

隐式操作若过于频繁使用,会让代码变得晦涩难懂。因此,在考虑添加新的隐式转换之前,请首先自问是否能够通过其他手段,诸如继承、混入组合或方法重载,达到同样的目的。如果所有这些都不能成功,并且你感觉代码仍有一些繁复和冗余,那么隐式操作或许正好能帮到你。

所以。。。谨慎使用,小心翻车,good luck!

参考文献

IMPLICIT CLASSES

scala的隐式转换学习总结(详细)

《Scala编程》

深入理解Scala的隐式转换

本文代码

Github仓库

转载请注明原文地址:https://liam-blog.ml/2019/09/28/scala-implicit/

查看更多博主文章

上一篇 下一篇

猜你喜欢

热点阅读