Scala implicit

概述

在 Scala 中 implicit 主要用于隐式转换和隐式参数,隐式转换可以自动完成类型转换,而隐式参数可以自动被传递给方法。

隐式转换

隐式转换作用有两点:

隐式转换成正确的类型

隐式转换成正确的类型(conversions to an expected type)的规则很简单:当编译器看到一个 X,但是实际上需要 Y,就会寻找能够将 X 转换成 Y 的隐式转换。

如 scala 允许将 Int 等范围较小的变量赋值给 Double 等范围较大的变量:

scala> val i = 2
i: Int = 2

scala> val d: Double = i
d: Double = 2.0

这便是通过隐式转换实现的。

隐式转换一次选择的接受者

隐式转换一次选择的接受者(conversions of the receiver of a selection)可以扩展已有类的功能。比如我们可以对 string 做 map 操作:

scala> "abc".map(_.toInt)
res0: scala.collection.immutable.IndexedSeq[Int] = Vector(97, 98, 99)

在 Scala 中,类 String 等价于 java.lang.String,并没有定义方法 map。编译器发现 String 不支持 map 后,会去寻找是否存在隐式转换能够将 String 转换成支持 map 方法的对象。scala.Predef 中就存在这样的隐式转换:

@inline implicit def augmentString(x: String): StringOps = new StringOps(x)

能够将 String 转换成 StringOps,而 StringOps 则支持 map 方法。

隐式转换规则

一般地,隐式转换需要符合以下规则:

标记规则(Marking Rule)

任何变量,函数或者对象都可以被标记成 implicit,但是只有被标记为 implict 的定义才有可能被编译器选中进行隐式转换。

作用域规则(Scope Rule)

Scala 编译器会在两个地方搜索隐式转换:

  1. 当前作用域
  2. 原类型或者目标类型的伴生对象

如果隐式转换不能在当前作用域直接访问到,那么编译器就不会考虑这样的转换。比如如果存在这样的隐式转换:someVariable.convert,只有通过 import someVariable.convert,编译器才可能用到。

不过,编译器还会去原类型或者目标类型的伴生对象搜索隐式转换定义,这些隐式转换不需要再导入当前作用域。比如 List 等集合的伴生对象都定义了 canBuildFrom:

object List extends SeqFactory[List] {
  /** $genericCanBuildFromInfo */
  implicit def canBuildFrom[A]: CanBuildFrom[Coll, A, List[A]] =
    ReusableCBF.asInstanceOf[GenericCanBuildFrom[A]]
  ...
}

List 的 map,flatMap 等方法都依赖 CanBuildFrom:

final override def map[B, That](f: A => B)(implicit bf: CanBuildFrom[List[A], B, That]): That
final override def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit bf: CanBuildFrom[List[A], B, That]): That
...

那么调用这些方法时,编译器就会在伴生对象中找到 CanBuildFrom 的定义。

只进行一次转换(One-at-a-time Rule)

编译器至多只会进行一次隐式转换。

显式优先(Explicits-first Rule)

只有代码出现了类型错误,编译器就才会尝试进行隐式转换。

隐式类

隐式类是使用 implicit 标记的类。对于隐式类,编译器会产生一个普通类和一个同名的隐式方法,并且该隐式方法会模仿类构造方法。 比如,我们通常可以通过如下方法构造一个 Map:

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

-> 并不是内置语法,而是类 ArrowAssoc 的一个方法:

implicit final class ArrowAssoc[A](private val self: A) extends AnyVal {
    @inline def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
    def →[B](y: B): Tuple2[A, B] = ->(y)
}

因为 ArrowAssoc 是一个隐式类,编译器会产生如下的类和隐式方法:

final class ArrowAssoc[A](private val self: A) extends AnyVal {
    @inline def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
    def →[B](y: B): Tuple2[A, B] = ->(y)
}

implicit def ArrowAssoc[A](x: A): ArrowAssoc[A] = 
    new ArrowAssoc(x)

类 ArrowAssoc 定义在 scala.Predef 中,所以我们可以在任何 scala 文件中使用 -> 的语法。

隐式类规则

隐式类有如下规则:

1. 隐式类只能定义在另一个 trait / class / object 中:
object Helpers {
  implicit class RichInt(x: Int) // OK!
}
implicit class RichDouble(x: Double) // ERROR, `implicit' modifier cannot be used for top-level objects
2. 隐式类主构造方法只能接受一个非隐式参数(non-implicit argument)作为第一个参数列表:
implicit class RichDate(date: java.util.Date) // OK!
implicit class Indexer[T](collecton: Seq[T], index: Int) // ERROR, implicit classes must accept exactly one primary constructor parameter
implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // OK, include additional implicit parameter list

不过隐式类还可以接受隐式参数作为其他参数列表。

3. 当前作用域中不可以有与隐式类重名的任何方法、成员或者对象:
object Bar
implicit class Bar(x: Int) // BAD!

val x = 5
implicit class x(y: Int) // BAD!

implicit case class Baz(x: Int) // ERROR, illegal combination of modifiers: implicit and case for: class Baz

case class 也不能定义成为隐式类,因为 case class 会生成伴生对象。

处理多个可能隐式转换

如果存在多个可能隐式转换,那么编译器会选择一个更具体的(more specific)。具体而言,如果以下条件满足任意一个,那么可以说一个隐式转换比另一个更具体:

  • The argument type of the former is a subtype of the latter’s.
  • Both conversions are methods, and the enclosing class of the former extends the enclosing class of the latter.

隐式参数

隐式参数能够让我们省略参数列表,由编译器提供我们省略的参数。比如,编译器可能可以将 someCall(a) 扩展成 someCall(a)(b),或者 new SomeClass(a) 扩展成 new SomeClass(a)(b)。

隐式参数只能定义在最后一个参数列表,该参数列表可以接受多个参数,但是只有第一个参数可以被标记为 implicit,如下:

object Main extends App {
  implicit val i: Int = 1
  implicit val j: String = " Hello, world"

  def fun(x: Int)(implicit y: Int, z: String): Unit =
    println(x + y + z)

  fun(0)
}

调用时,我们可以省略最后整个参数列表,由编译器查找合适的变量补上,完成调用。

这样的变量需要满足:

  • is marked implicit
  • has a type compatible with T
  • is visible at the point of the function call, or is defined in a companion object associated with T

隐式参数常用于函数其他参数提供类型信息,一个例子是 CanBuildFrom

参考

  1. Programming in Scala
  2. IMPLICIT CLASSES
  3. SIP-13 - Implicit classes

Previous post: Scala 中的 Monads

Next post: 有括号方法和无括号方法区别