隐式转换系统

Scala的隐式转换系统提供了一套定义良好的查找机制,让编译器能够调整代码。当用Scala写代码时可以故意漏掉一些信息,而让编译器去尝试在编译期自动推导出来。

Scala编译器可以推导下面两种情况:

  • 缺少参数的方法调用或构造器调用
  • 缺少了从一个类型到另一个类型的转换,这也适用于需要转换才能进行的对象方法调用

使用隐式转换能够减少代码,能够向已有类型中注入新的方法,也能够创建领域特定语言(DSL)。

1 隐式参数

1.1 隐式参数的例子


In [1]:
// 需求场景:
// 提供一个计算销售税的方法
// 条件:
// 1. 某些应用需要知道当前事务发生的具体地点,以便增收税
// 2. 为了促进购物消费,某些地方可能将年假最后几天定位“免税期”

def calcTax(amount: Float)(implicit rate: Float): Float = amount*rate

object SimpleStateSalesTax {
    implicit val rate: Float = 0.05F
}

case class ComplicatedSalesTaxData(
    baseRate: Float,
    isTaxHoliday: Boolean,
    storeId: Int
)

object ComplicatedSalesTax {
    private def extraTaxRateForStore(id: Int): Float = {
        // 可以通过id推断商铺地点,再计算附加税
        0.0F
    }
    
    implicit def rate(implicit cstd: ComplicatedSalesTaxData): Float =
        if (cstd.isTaxHoliday) 0.0F
        else cstd.baseRate + extraTaxRateForStore(cstd.storeId)
}


defined function calcTax
defined object SimpleStateSalesTax
defined class ComplicatedSalesTaxData
defined object ComplicatedSalesTax

In [2]:
{
    import SimpleStateSalesTax.rate
    
    val amount = 100F
    println(s"Tax on $amount = ${calcTax(amount)}")
}


Tax on 100.0 = 5.0
import SimpleStateSalesTax.rate
amount: Float = 100.0F

In [3]:
{
    import ComplicatedSalesTax.rate
    implicit val myStore = ComplicatedSalesTaxData(0.06F, false, 1010)
    
    val amount = 100F
    println(s"Tax on $amount = ${calcTax(amount)}")
}


Tax on 100.0 = 6.0
import ComplicatedSalesTax.rate
myStore: ComplicatedSalesTaxData = ComplicatedSalesTaxData(0.06F, false, 1010)
amount: Float = 100.0F

1.2 implicitly方法

使用Predef对象定义的implicitly方法与附加类别签名结合,可以使用一种便捷的方式定义一个接受参数化类型隐式参数的函数


In [4]:
import math.Ordering

case class MyList[A](list: List[A]) {
    def sortBy1[B](f: A => B)(implicit ord: Ordering[B]): List[A] = 
        list.sortBy(f)(ord)
    
    def sortBy2[B: Ordering](f: A => B): List[A] =
        list.sortBy(f)(implicitly[Ordering[B]])
}


import math.Ordering
defined class MyList

In [5]:
val list = MyList(List(1, 3, 5, 2, 4))


list: MyList[Int] = MyList(List(1, 3, 5, 2, 4))

In [6]:
list sortBy1 (i => -i)


res5: List[Int] = List(5, 4, 3, 2, 1)

In [7]:
list sortBy2 (i => -i)


res6: List[Int] = List(5, 4, 3, 2, 1)

MyList类提供了两种sorBy方法:

  • 第一种使用常规方法,该方法接受一个类型为Ordering[B]的隐式值作为输入,在当前作用域中一定存在某个Ordering[B]的对象实例,该实例清楚地知道对B类型对象如何进行排序。所以,这个例子中,上下文限定了B对实例排序的能力。
  • 第二种方法提供了简化版的语法,类型参数B: Ordering被上下文定界(context bound),它安置隐式参数列表将接受Ordering[B]实例。

implicitly方法会对传给函数的所有标记为隐式参数的实例进行解析。

1.3 绕开类型擦除带来的限制

object M {
    def m(seq: Seq[Int]): Unit = println(s"Seq[Int]: $seq")

    def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")
}

将会出现这样的报错:

Compilation Failed
Main.scala:50: double definition:
def m(seq: Seq[Int]): Unit at line 49 and
def m(seq: Seq[String]): Unit at line 50
have same type after erasure: (seq: Seq)Unit
    def m(seq: Seq[String]): Unit = println(s"Seq[String]: $seq")
        ^

因为上面的两个m方法的字节码是一样的,编译器不允许同时出现这些方法定义。

不过我们可以添加隐式参数来消除这些方法的二义性。


In [8]:
object M {
    implicit object IntMarker
    implicit object StringMarker
    
    def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit = println(s"Seq[Int]: $seq")
    def m(seq: Seq[String])(implicit s: StringMarker.type): Unit = println(s"Seq[String]: $seq")
}


defined object M

In [9]:
import M._


import M._

In [10]:
m(List(1, 2, 3))


Seq[Int]: List(1, 2, 3)


In [11]:
m(List("one", "two", "three"))


Seq[String]: List(one, two, three)


In [12]:
IntMarker


res11: IntMarker.type = cmd7$$user$M$IntMarker$@39546a

In [13]:
StringMarker


res12: StringMarker.type = cmd7$$user$M$StringMarker$@a1f49b

为了尽量避免使用Int和String这样常用类型作为隐式参数和对应值(这些类型还可能出现多处定义,而导致二义性和编译器抛出错误),比较安全的做法是专门设计特有的类型作为隐式参数。

1.4 @implicitNotFound注解

@implicitNotFound注解告诉编译器在不能构造出带有该注解的类型的参数时给出错误提示。这样做事给程序员有意义的错误提示。

比如CanBuildFrom构造器和<:<类有相关注解:

@implicitNotFound(msg = "Cannot construct a collection of type ${To} with elements of type ${Elem} based on a collection of type ${From}.")
trait CanBuildFrom[-From, -Elem, +To] {...}
@implicitNotFound(msg = "Cannot prove that ${From} <:< ${To}.")
sealed abstract class <:<[-From, +To] extends (From => To) with Serializable

2 隐式参数的规则

  • 只有最后一个参数列表允许出现隐式参数
  • implicit关键字必须出现在参数列表的最左边,而且只能出现一次
  • 假如参数列表以implicit关键字开头,那么所有的参数都是隐式的