Simple Scala: Implicits

简介

隐式转换是为现有的类库增加功能的一种方式,用java的话,只能用工具类或者继承的方式来实现,而在scala则采用隐式转化的方式来实现。

  • scala隐式转换可以把一个类的实例当作另一个类的实例,这样一来,通过在facade里隐式封装实例,无需修改原来的类,就可以将方法附着于对象上。这种技巧可以用于创建DSL.
  • 在Predef对象里,scala定义了一些隐式转换,scala会默认导入它们,这样的话,比如写1 to 3时,scala会隐式将1从Int转换为RichInt类型,然后调用to()方法.
  • scala一次至多应用一个隐式转换,在当前范围内,如果发现通过类型转换有助于操作、方法调用或类型转换的成功完成,就会进行转换,不再继续往外找.
    • 位于源或者目标类型的companion object中的隐式函数
    • 位于当前作⽤用域可以以单个标识符指代的隐式函数

说明:

  • 如果代码能在不使用隐式转换的前提下通过编译,则不会使用隐式转换
  • 编译器不会同时使用多个隐式转换
  • 存在二义性的隐式转换不被允许
  • implicit关键字只能用来修饰方法、变量(参数)和伴随对象
  • 隐式转换的方法(变量和伴随对象)在当前范围内才有效。如果隐式转换不在当前范围内定义(比如定义在另一个类中或包含在某个对象中),那么必须通过import语句将其导入。
  • 定义函数时参数可以声明为implicit,但implicit关键字必须出现在首位,并且对所有的参数有效,不能给部分参数声明为implicit,比如:

    def maxFunc(implicit a: Int, b: Int) = i1 + i2
    

    maxFunc的拥有两个隐式参数a和b,不能按下面这样写:

    def maxFunc( a: Int, implicit b: Int) = i1 + i2
    

    也不能按下面这样写:

    def maxFunc(implicit a: Int, implicit b: Int) = i1 + i2
    

    如果只想声明一个参数为隐式参数,则需要使用curry方式,比如:

    def maxFunc(implicit a: Int)(b: Int) = i1 + i2
    

    此时maxFunc只拥有一个隐式参数a.

  • 匿名函数不能声明隐式参数,比如:

    val f = (implicit s: String) => s+1
    
  • 如果一个函数带有隐式参数,则不能通过 _ 获得该函数引用,这样做将会编译失败:

    def maxFunc(implicit i1: Int, i2: Int) = i1 + i2
    val f = maxFunc _     // 编译错误
    

参数隐式转换

假如我们我们有一个方法用于计算销售税,但是比率是隐式转换的:

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

如果调用该方法,当前作用域内的隐式转换值会被使用:

implicit val currentTaxRate = 0.8F 
...
val tax = calcTax(0000F)               // 4000.0

对于简单的情况,可能一个固定的浮点值作为缴税比率就足够了.但是,程序可能需要知道在哪交易,添加城市税等等.同时有些地方有时又会实行免税期以促进消费.
这些同样可以使用隐式转换,下面是一个实例:

// Never use Floats for money:
def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate

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

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

object ComplicatedSalesTax {
    private def extraTaxRateForStore(id: Int): Float = {
        // From id, determine location, then extra taxes...
        0.F
    }
    implicit def rate(implicit cstd: ComplicatedSalesTaxData): Float = 
        if (cstd.isTaxHoliday) 0.F
        else cstd.baseRate + extraTaxRateForStore(cstd.storeId)
}

{
    import SimpleStateSalesTax.rate
    val amount = 00F
    println(s"Tax on $amount = ${calcTax(amount)}")
}

{
    import ComplicatedSalesTax.rate
    implicit val myStore = ComplicatedSalesTaxData(0.6F, false, 1010)
    val amount = 00F
    println(s"Tax on $amount = ${calcTax(amount)}")
}

使用implicitly

Predef定义了一个implicitly方法,它结合类型签名另外提供有用的简写方法定义接受一个单一的隐式参数,该参数是参数化的类型情况的方法签名.
考虑下面的实例,是谁包装了List的sortBy方法:

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]])
}

val list = MyList(List(1,3,5,2,4))
list sortBy1 (i => -i) 
list sortBy2 (i => -i)

List.sortBy是很多集合排序方法中的一个,它接受一个函数,然后将参数转换成满足math.Ordering要求的类型,类似于java的Comparable抽象.必须提供一个隐式参数以知道如何排序B类型的实例.
MyList展示了两种不同的思路用于编写类似于sortBy的方法.第一个sortBy1使用了我们已经知道的语法,该方法额外接收一个Ordering[B]类型的隐式参数值.因此为了使用sortBy1,当前作用域内必须有一个实例以知道如何排序B类型的实例.我们说B绑定了一个上下文,这个例子中是排序实例的能力.
这种习惯用法非常普遍,scala提供了一种简写语法,即第二种sortBy2的实现.参数类型 B : Ordering被称为上下文绑定.

隐式参数方案

明智谨慎的使用隐式转换很重要,过度的使用则使人难以理解代码的真正意图.
隐式参数的使用基本有两种好处:第一类是样板消除,隐式提供上下文信息.第二类是约束,限制允许的类型以减少错误.

执行上下文

在关于Future的实例介绍中,隐式参数列表用于向 Future.apply方法传送一个ExecutionContext:

apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]

一些其他方法也有这样的隐式参数.
当我们调用这些方法时并不需要指定ExecutionContex,但是编译器会使用我们已经引入的一个ExecutionContex:

import scala.concurrent.ExecutionContext.Implicits.global

传送一个执行上下文是隐式参数的推荐用法,其他的一些上下文实例包括事务,数据库连接,线程池,用户会话等.合理使用隐式参数可以创建清晰的API.

权限控制

除了传送上下文环境,隐式参数还可以用于控制权限.
比如一个隐式用户会话可能会包含一些API或数据对于该用户的可见性授权令牌.假设你正在构建一个用户登录菜单,已登录用户显示某些选项而未登录用户只显示登录选项:

def createMenu(implicit session: Session): Menu = { 
    val defaultItems = List(helpItem, searchItem) 
    val accountItems =
        if (session.loggedin()) List(viewAccountItem, editAccountItem)
        else List(loginItem) 
    Menu(defaultItems ++ accountItems)
}

Constraining Allowed Instances: 约束允许的实例

如果我们有一个参数化类型的方法并且想要限制该参数类型的种类.

如果我们要限制的类型都属于常用基本类型的子类,可以使用面向对象技术以避免implicit.

object manage {
    def apply[R <: { def close():Unit }, T](resource: => R)(f: R => T) = {...} ...
}

上面的代码中,R的类型必须是方法 def close():Unit 的子类型,我们可以实现一个 Closable 特质:

trait Closable { 
    def close(): Unit
}
...
object manage {
    def apply[R <: Closable, T](resource: => R)(f: R => T) = {...}
... }

….