Home > Archives > Scala基础之隐式转换(implicit-conversion)

Scala基础之隐式转换(implicit-conversion)

Published on

无论是对于初学者,还是对于有经验的Scala程序员,隐式转换都是一个必须掌握的技巧。它可以进行类型转换,方法调用还有类功能增强。如果高级一点,就是在类型类中的使用。

隐式转换是指Scala编译器会帮助完成指定的”转换”,从而减少重复代码并且在一定程度上扩展现有库的功能。隐式参数就是不需要你显示传递参数(Scala编译器会帮你传递当然你也可以显示的传递)。

基本使用

(1) 减少重复代码

如果class C的某个实例o调用了方法m,即o.m, 但实际上o并没有m方法。这时候Scala编译器就会去寻找能够把o转成支持m方法调用的一个类型。 在Java Swing中我们会给组件注册监听事件,通常会有如下代码:

JButton button = new JButton("press me");
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Click");
    }
});

如果以Scala的角度考虑(函数式), 我们可以看出ActionListener中代码是非常模式化的,就是生成ActionListerner的实例并且重写actionPerformed方法。

    button.addActionListener({
        (_: ActionEvent) => println("I am pressed")
    })
    // 但是这种写法会报编译类型错误的
    
    // 增加隐式方法为我们完成这次转换
    implicit def actionEventFuncToActionListener(f: ActionEvent => Unit) =
        new ActionListener {
            override def actionPerformed(e: ActionEvent): Unit = f(e)
        }

利用隐式转换避免每次注册监听事件都要重复实现ActionListener,当然仅仅是对于通用的逻辑。

(2) 功能扩展

比如说我们想扩展某个jar中类的功能,就可以采用此方法。

// 给String添加新方法
class StringImprovement(s: String) {
    def increment = s.map(one => (one + 1).toChar)
}
implicit def stringIncrement(s: String) = new StringImprovement(s)

"abc".increment

//还有一个更为常见
package scala
object Predef {
    class ArrowAssoc[A](x: A) {
      def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)
    }
    implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] =
      new ArrowAssoc(x)
    ...
}

Map(1 -> "one", 2 -> "two") 

隐式参数

编译器会自动帮你传递这个参数,如果找不到就会报错。简单来说就是someCall(a)在实际调用的时候会变成someCall(a)(b)

class Welcome(val msg: String)
object Greeter {
    def greet(name: String)(implicit welcome: Welcome) = {
        s"${name}, ${welcome}" 
    }
}

// 显示调用
Greeter.greet("allen")(new Welcome("欢迎回来"))

object GreetingSetting {
    implicit val welcome = new Welcome("欢迎回来")
} 

//引入隐式参数
import GreetingSetting._
Greeter.greet("allen")

Scala内置的CanBuildFrom就利用了这一点

trait CanBuildFrom[-From, -Elem, +To] {}

def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {}

视界(View Bound)

对于O <% T, 只要O可以被隐式转换成T或者O是T的子类或者就是T类型,那么我就可以随意使用O。

def maxList[T <% Ordered[T]](elements: List[T]): T = {
    elements match {
        case Nil => throw new NoSuchElementException("empty list!")
        case head :: Nil => head
        case head :: tail => {
            val maxRest = maxList(tail) 
            if (head > maxRest) head
            else maxRest
        }
    }
}

// Int => intWrapper(隐式转换成RichInt, 而RichInt是Ordered的子类)
maxList(List(1,2,3))


// 下面两种写法等价
def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) 
    = seq.indexOf(value)
def getIndexViaViewBound[T, CC <% Seq[T]](seq: CC, value: T) 
    = seq.indexOf(value)

getIndexViaContextBound("abc", 'c')
// 隐式转换搜索的过程
implicit def wrapString(s: String): WrappedString = 
    if (s ne null) new WrappedString(s) else null

// IndexSeq有indexOf方法的
// "abc" ->  wrapString("abc")
class WrappedString(val self: String) extends AbstractSeq[Char] 
with IndexedSeq[Char] with StringLike[WrappedString] {

隐式类型的规则

> 只有使用implicit标记的定义(val, def, class)才会被编译器当作隐式类型去使用

> 插入的隐式转换必须以单一标识符的形式存在于作用域中,或是与源类型或目标类型相关联。

前半句的意思就是x + y可以被转换成convert(x) + y, 但是不会被转换成SomeVariable.convert(x) + y。如果非要使用后一种必须要显示的引入进来。

后半句的意思是:

object Dollar {
    implicit def dollarToEuro(dollar: Dollar): Euro...
}
object Euro {
    implicit def dollarToEuro(dollar: Dollar): Euro...
}
class Dollar {}

def testCompanionScope(euro: Euro) = ...

如上面的代码,如果某个需要一个Euro作为参数方法被传入了Dollar类型的,这时Scala编译器会搜索Dollar(源类型)或Euro(目标类型)的伴生对象以锁定相应的隐式转换。

> 需要隐式类型的地方,一次只能进行一次转换

也就是不可能有x + y被编译器隐式转换成convert1(convert2(x)) + y, 这种写法可能会导致实际运行的代码跟你预期的有很大的不同,而且使代码变得不清晰。

> 如果通过编译器检测,那么隐式类型便不会被用到

> 如果某个作用域有多个隐式转换,我们可以通过显示的声明来控制哪个隐式转换需要用到

隐式类型的寻找

> 搜索当前作用域

implicit val limit = 10
def paginateByXX(search: String)(implicit limit: Int) = ???

> 显示的引入

import scala.collection.JavaConversions.mapAsScalaMap 
val env = System.getenv
env.apply("USER")

> 某个类的伴生对象(Dollar, Euro所描述的)

class A(val n: Int) {}
object A {
implicit val ord: Ordering[A] = new Ordering[A] {
    def compare(x: A, y: A): Int = x.n - y.n
}
}

// def sorted[B >: A](implicit ord: Ordering[B]): Repr...
List(new A(3), new A(5)).sorted

很明显上面的sorted方法需要传入一个隐式的ord参数,但是Ordering[A]根本没有这样的转换,这时候Scala编译器就会去类型参数A中去寻找即A的伴生对象中定义的隐式ord。

关于作用域,下面的案例也能说明问题:

trait FKTC[T] {
    def value: T
}

// companion here is for Scala Compiler to find 
object FKTC {
  implicit val defaultInt = new FKTC[Int] {
    def value = 5
  }
  
  implicit def listInt[T: FKTC] = new FKTC[List[T]] {
     def value = implicitly[FKTC[T]].value :: Nil
  }
}

object FKTCTest {

    implicit val defaultInt = new FKTC[Int] {
        def value = 43
    }
    
    def default[T: FKTC] = implicitly[FKTC[T]].value

    default[Int] 
    default[List[Int]] 
}

defaultInt的存在与否,将会影响最后两行的结果,如果存在则结果分别为43, List(43); 反之则为5, List(5)。

在实际开发过程中,Scala隐式转换使用起来还是非常方便的,只要注意好作用域,基本不会有太大的问题。当然也有,隐式转换的层次太多反而让人理解起来有点困难的,典型的就是Spray Directive。所以,有时要注意权衡一下简洁和可读性。

参考

> 隐式转换的寻找规则

> Programming In Scala - Implicit Conversions and Parameters(Chapter 21)

> 隐式转换 vs 类型类

声明: 本文采用 BY-NC-SA 授权。转载请注明转自: Allen写字的地方