Jade Dungeon

隐式转换

隐式转换

隐式转换函数

Scala里有一个很有用的特质RandomAccessSeq[T]提供了可以随机访问的序列。它有很多 功能,所以它的子类都自动继承这些功能。但是没有混入这个特质的类,比如String 就用不到那些方便的功能。

隐式转换函数(implicit conversion function)可以理解为是一个以String类实例为 参数来构造一个对应的RandomAccessSeq实例的函数。格式以implicit开头:

  implicit def stringWrapper(s: String) = 
    new RandomAccessSeq[Char] {
      def length = s.length
      def apply(i: Int) = s.charAt(i)
    }

然后就可以转换它了:

  scala> stringWrapper("abc123") exists (_.isDigit)
  res0: Boolean = true

之所以称为隐式转换,就是可以在需要的时候自动地转换为可用的类型:

  scala> "abc123" exists (_.isDigit)
  res1: Boolean = true

顺便提一下:Predef对象已经用类似的方式定义了stringWrapper转换,所以其实不用 定义前面的stringWrapper函数也可以隐式转换了。

隐式转换会引起类型变化

隐式转换很有用,但有时要注意隐式转换相引起的类型变化:

"mon".reverse == "mon" // false

不相等的原因是因为类型已经变为RichString,这样就是相等的了:

"mon".reverse.toString == "mon" // false

隐式操作规则

标记规则

只有标记为implicit的定义才是可用的。可以用来标记任何变量、函数或对象定义。

作用域规则

必须以单一标识符的形式处于作用域中,或与转换的源或目标类型关联在一起。

所以不能用aaa.convert(x),这不是单一的。要先import aaa,然后convert(x), 这样才是单一的。

「单一标识符」规则有个例外。转换的「目标」与「源」的类型的「伴生对象」中的隐式转换定义 会被编译器找到。如从Dollar转为Euro,可以把隐式转换放在这两者之一的的伴生对象 中:

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

这样就不用手动引入了。

还可以排除某个给你带来麻烦的隐式转换函数。如不希望把整数转为双精度浮点,可以 排除一个隐式转换:

import aa.TransTools.{int2Double => _,_}

无歧义规则

不能有其他转换,如果有两个可用的从类A到类B的转换,会报错。可以移除一个转换函数, 或是显式指明一个方法,如:convertFunc2(x) + y

单一调用原则

不会嵌套地转换,如:convert1(convert2(x)) + y

显式操作先行规则

如果不用转换类型就可以用,编译器不会再画蛇添足地转换。

这是一个供程序员把握的度:如果代码太冗长,用隐式转换来精简代码;如果代码看起来 太简单不明确,用显式的转换来减少歧义和二义性。

隐式转换的命名

转换方法的命名可以随意,但要考虑到两个情况:

  • 是不是需要在方法应用中明确写明
  • 决定在哪个隐式转换在程序的任何地方都有效。

拿第二点来说,设一个对象带两个隐式转换:

  object MyConversions {
    implicit def stringWrapper(s: String):
        RandomAccessSeq[Char] = ...
    implicit def intToString(x: Int): String = ...
  }

现在只需要stringWrapper,不想要intToString。可以只引入一个:

  import MyConversions.stringWrapper
  ... // code making use of stringWrapper

这样的情况下会用到转换方法的名字。

一般约定俗成的习惯是A2B这样的形式。如int2String

隐式转换的应用场景

Scala会在三种情况下触发隐式转换:转换为需要的类型、指定(方法)调用者的转换、 隐式参数。接下来的三节分别讨论这三种情况。

隐式转换为期望的类型

有一个双精度数,但是表达式要用到整数,所以就要隐式转换:

  scala> val i: Int = 3.5
  <console>:5: error: type mismatch;
   found   : Double(3.5)
   required: Int
         val i: Int = 3.5
                      ^

定义隐式转换就可以用了:

  scala> implicit def doubleToInt(x: Double) = x.toInt
  doubleToInt: (Double)Int

  scala> val i: Int = 3.5
  i: Int = 3

相当于:

  val i: Int = doubleToInt(3.5)

因为从DoubleInt会丢失精度,所以Predef中没有默认定义;但是反过来在 Predef中已经有定义了:

  implicit def int2double(x: Int): Double = x.toDouble

转换方法调用的接收者

例如,java.io.File类是没有read()方法来读取整个文件的:

val contents = new File("Readme.txt").read

在Scala中可以定义一个支持read()方法的强化类:

class RichFile(val from: File) {
	def read = Source.fromFile(from.getPath).mkstring
}

然后定义一个从FileRichFile的隐式转换:

implicit def file2RichFile(from: File) = new RichFile(from)

这样就可以在File类上调用read方法了:

val contents = new File("Readme.txt").read
与新类型的交互操作

给新类型加上它没有实现的功能。以实数类为例:

  class Rational(n: Int, d: Int) {
    ...
    def + (that: Rational): Rational = ...
    def + (that: Int): Rational = ...
  }

加法可以用RationalInt类型作为参数:

  scala> val oneHalf = new Rational(1, 2)
  oneHalf: Rational = 1/2

  scala> oneHalf + oneHalf
  res4: Rational = 1/1

  scala> oneHalf + 1
  res5: Rational = 3/2

但是一个Int类型的加法没有办法以Rational类型为参数:

  scala> 1 + oneHalf
  <console>:6: error: overloaded method value + with
  alternatives (Double)Double <and> ... cannot be applied
  to (Rational)
       1 + oneHalf
         ^

办法是让Int转为Rational

  scala> implicit def intToRational(x: Int) = 
       |   new Rational(x, 1)
  intToRational: (Int)Rational
  
  scala> 1 + oneHalf
  res6: Rational = 3/2

相当于:

  intToRational(1) + oneHalf
模拟新的语法

隐式转换还可以用来模拟新的语法。以我们熟悉的Map构建为例:

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

其实Map对象里的->根本就不是内建语法。实际上是定义在scala.Predef中的类 ArrowAssoc的方法。还有定义了从AnyArrowAssoc的隐式转换。定义是:

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)
  ...

}

拿上面的1 -> "one"来说。

  • 其实是从先从1ArrowAssoc[Int]隐式转换。
  • 再调用ArrowAssoc[Int]类型的->方法,返回了一个Touble2

隐式参数与隐式值

方法参数列表用implict修饰后,编译器会查找隐式值(即参数的默认值)。因为 implict会把整个参数列表都作为可隐式参数,一般与柯里化结合把参数列表分成多个 参数列表。

举例有一个类实现了命令行提示符:

  class PreferredPrompt(val preference: String)
声明参数列表为隐式

还有一个Greeter类的方法greet有两个参数列表。分别是用户名和前面的命令行 提示符类:

  object Greeter {
    def greet(name: String)(implicit prompt: PreferredPrompt) {
      println("Welcome, "+ name +". The system is ready.")
      println(prompt.preference)
    }
  }

后一个参数列表被声明为implicit,表示可以隐式提供。当然要显式提供也可以:

  scala> val bobsPrompt = new PreferredPrompt("relax> ")
  bobsPrompt: PreferredPrompt = PreferredPrompt@ece6e1

  scala> Greeter.greet("Bob")(bobsPrompt)                    
  Welcome, Bob. The system is ready.
  relax> 
定义隐式值(实参的默认值)

implicit修饰隐式值,即实参的默认值:

  object JoesPrefs {
    implicit val prompt = new PreferredPrompt("Yes, master> ")
  }

注意它本身也要被定义为implicit,不然是不会被用来作为隐式变量的。而且如果不是在 同一作用域的话,也不能用:

  scala> Greeter.greet("Joe")
  <console>:7: error: no implicit argument matching parameter
    type PreferredPrompt was found.
         Greeter.greet("Joe")
                 ^

不过引入后就可以用了:

  scala> import JoesPrefs._         
  import JoesPrefs._

  scala> Greeter.greet("Joe")
  Welcome, Joe. The system is ready.
  Yes, master> 
使用隐式函数与隐式参数

以税费作为例子,对于隐式的参数,如果能找到一个隐式值作为默认值,就可以成功调用:

// 函数,用来计算税额
scala> def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate
calcTax: (amount: Float)(implicit rate: Float)Float

// 作用域里有隐式的值,就可以作为默认值 
scala> implicit val rate: Float = 0.05F
rate: Float = 0.05

scala> calcTax(100)
res16: Float = 5.0

对于更加复杂的情况,隐式地调用可以多级调用,例如:

// 实体类,具体消费记录,包含基本税率,是否免税,卖家的ID
scala> case class SalesInfo(baseRate: Float, isTaxFree: Boolean, storeId: Int)
defined class SalesInfo

scala> object SalesTaxUtil {
     |   // 按卖家ID,算出额外税率
     |   private def getStoreTaxRate(id: Int): Float =
     |     if (id < 500) 0.0F else 0.12F
     |
     |   // 按消费记录来算实际的税率,消费记录是隐式参数
     |   implicit def rate(implicit info: SalesInfo): Float =
     |     if (info.isTaxFree) 0.0F else
     |     info.baseRate + getStoreTaxRate(info.storeId)
     | }
defined object SalesTaxUtil

// 函数,用来计算税额,税率是隐式参数
scala> def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate
calcTax: (amount: Float)(implicit rate: Float)Float

// 隐式的值,用来测试的一条消费记录
scala> implicit val rec1 = SalesInfo(0.06F, false, 1010)
rec1: SalesInfo = SalesInfo(0.06,false,1010)

// 隐式的函数,一定要手动导入,才能用到默认的rate函数
scala> import SalesTaxUtil.rate
import SalesTaxUtil.rate

scala> calcTax(100.00F)
res15: Float = 18.0

这里calcTax的隐式参数rate: Float找到了类型为Float的SalesTaxUtil.rate(), 这个函数也有一个隐式参数info: SalesInfo又找到了同类型的隐式值rec1。 这说明了隐式调用可以多级进行。

implicit会作用于整个参数列表

注意implicit关键字作用于全体参数列表而不是单独参数。下面的例子中Greetergreet方法最后的参数列表再次被标记为implicit,它有两个参数: prompt: PrefferredPromptdrink: PrefferedDrink

  class PreferredPrompt(val preference: String)
  class PreferredDrink(val preference: String)

  object Greeter {
    def greet(name: String)(implicit prompt: PreferredPrompt,
        drink: PreferredDrink) {

      println("Welcome, "+ name +". The system is ready.")
      print("But while you work, ")
      println("why not enjoy a cup of "+ drink.preference +"?")
      println(prompt.preference)
    }
  }

  object JoesPrefs {
    implicit val prompt = new PreferredPrompt("Yes, master> ")
    implicit val drink = new PreferredDrink("tea")
  }

伴生对象中定义了两个隐式的变量,只要它们不作为单一标识符处于作用域内,不然就不能 用来填充缺少的参数列表:

  scala> Greeter.greet("Joe") 
  <console>:8: error: no implicit argument matching parameter
    type PreferredPrompt was found.
         Greeter.greet("Joe")
                 ^

import导入:

  scala> import JoesPrefs._
  import JoesPrefs._

可以自动填充了:

  scala> Greeter.greet("Joe")(prompt, drink)
  Welcome, Joe. The system is ready.
  But while you work, why not enjoy a cup of tea?
  Yes, master> 


  scala> Greeter.greet("Joe")
  Welcome, Joe. The system is ready.
  But while you work, why not enjoy a cup of tea?
  Yes, master> 

注意这里的没有用String这样的常用类型来作为promptdrink的类型。就是为了 防止被过多地匹配到隐式转换。

参数类型不能相同

隐式参数的默认值是按类型来区分的,所以每种类型只能有一个参数:

def myFunc(value: Int)(implicit left: String, right: String)  // error

这样是不行的,leftright两个参数类型相同。

用隐式转换代替变化类型参数

以实现排序功能为例,类型参数与隐式类型转换代表了两种不同的思路:

  • 类型参数的上界限制:规定得到的类型必须是具有排序功能的类的子类。
  • 隐式类型转换:任何类都可以,但要提供一个从得到类转为具有排序功能的类的方法。

从上面可以看出,隐式转换可以应用的范围更广。

例子:

下面的方法返回两个数中较小的一个:

def smaller[T](a: T, b: T) = if (a < b) a else b  // Error

上面的代码有错,因为对于任意类型T,不能保证都支持<()方法。解决方法可以通过 变化类型的限制(泛型这一部分已经讲过),也可通过提供一个隐式转换函数来把类型T 转为支持<()操作的类型,比如实现了Ordered特质:

def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = 
	if (order(a) < b) a else b

类型Ordered[T]表示操作方法支持类型T。在这种情况下,Predef对象对很多常见 类型都已经定义的T => Ordered[T]的转换,包括已经实现Order[T]Comparable[T] 的类型。所以很多情况下已经有默认的转换实现了,下面的代码直接就可以用:

smaller(40,2)
smaller("Hello", "world")

如果有一个自定义的类,比如学生类:

class Student(val name: String, val age: Int) {
  override def toString = "{name: " + name + ", age: " + age + "}"
}

val a  = new Student("morgan", 10)
val b  = new Student("jade", 9)
val c  = new Student("wendy", 7)
val d  = new Student("Teo", 12)

我们要按年龄比较:

class StudentAgeOrdered(s: Student) extends Ordered[Student] {
  def compare(that: Student) = s.age - that.age
}

再实现隐式转换:

implicit def Student2StudentAgeOrdered(s: Student) = new StudentAgeOrdered(s)

这样就实现了比较的功能:

scala> maxListImpParm(a :: b :: c :: d :: Nil)
res11: Student = {name: Teo, age: 12}

再来看一个例子,它返回传入的列表参数中的最大元素。先看一个通过类型参数限制的 实现版本:

  def maxListUpBound[T <: Ordered[T]](elements: List[T]): T = 
    elements match {
      case List() =>
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
        val maxRest = maxListUpBound(rest)
        if (x > maxRest) x
        else maxRest
    }

注意类型参数已经被限制为T <: Ordered[T],就是说列表的元素T要实现Ordered[T] 特质。所以列表是可排序元素的列表。但是缺点是Int列表不行,因为Int没有实现特质 Ordered,所以不是Ordered[T]的子类。

更加泛用的隐式转换版本,添加一个把T转为Ordered[T]的函数作为参数:

下面的例子中第二个参数被标记为implicit

  def maxListImpParm[T](elements: List[T])
        (implicit orderer: T => Ordered[T]): T =

    elements match {
      case List() => 
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
        val maxRest = maxListImpParm(rest)(orderer)
        if (orderer(x) > maxRest) x
        else maxRest
    }

要排序的列表是必须显式提供的,所以元素的类型T是在编译时就确定的。确定了类型T 之后就可以判断T => Ordered[T]类型的隐式定义是否存在于作用域中。如果存在就隐式 传入排序函数order

这种方式在很多Scala的通用库中也用到,它们提供了隐式的排序方法。所以我们上面写的 方法可以用在很多类型上:

  scala> maxListImpParm(List(1,5,10,3))
  res10: Int = 10

  scala> maxListImpParm(List(1.5, 5.2, 10.7, 3.14159))
  res11: Double = 10.7

  scala> maxListImpParm(List("one", "two", "three"))
  res12: java.lang.String = two
隐式参数样式规则

就是在要用到隐式转换的地方最好是从自定义的类型开始转而不是从String这样的很常见 的类型开始转。如果maxListImpParm直接写成下面这样的方法签名:

  def maxListPoorStyle[T](elments: List[T])
        (implicit orderer: (T, T) => Boolean): T

这样(T,T) => Boolean的类型太常见了,很容易被匹配到到不希望匹配到的方法上去。

所以简单地说:至少用一个确定的名称为隐式类型参数命名。

视图界定

可以把T <% Ordered[T]理解为:T是能被当作Ordered[T]的任何类型。编译器将 调用声明在Predef中的隐式鉴别函数:

implicit def identity[A](x: A): A = x

如果传入的类型正好就是Ordered[T],那上面的转换什么也不做,只是简单地把传入的 参数再返回出来。

前面的例子还可以再用隐式操作强化。如果把implicit用在参数上,编译器不仅会尝试用 隐式值补足这个参数,还会把这个参数当作可用的隐式操作而使用于方法体中。因此方法体 中orderer的两处应用都可以被省略。

  def maxList[T](elements: List[T])
        (implicit orderer: T => Ordered[T]): T =

    elements match {
      case List() => 
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
        val maxRest = maxList(rest)  // (orderer) is implicit
        if (x > maxRest) x           // orderer(x) is implicit
        else maxRest
    }

编译器会发现上面代码的类型不能匹配。比如如T类型的x不存在>方法,所以 x > maxRest不起作用。

但编译器在这个时候并不会马上停止,而是先查找能修复这个问题的隐式转换。

在这个例子中换成了orderer(x) > maxRest,并且同样把maxList(rest)换成了 maxList(rest)(ordered)

回过来看maxList方法中没有提到有ordered参数的地方,所有对ordered的使用都是 隐式的。这是一个很常用的代码模式:隐式参数只是用来转换,所以它本身也可以被隐式地 使用。

在现在的版本,因为参数名没有被显式调用,所以名称也可以随便定。如,只要不改变 maxList的方法体,对于只改变参数名称来说,方法的行为没有任何改变:

  def maxList[T](elements: List[T])
        (implicit converter: T => Ordered[T]): T =
    // same body...

改成这样也没有问题:

  def maxList[T](elements: List[T])
        (implicit iceCream: T => Ordered[T]): T =
    // same body...

因为这样的方法非常常用,所以Scala可以让代码省略这个参数的名称并使用视图界定缩短 方法头。maxList方法签名可以是这样:

  def maxList[T <% Ordered[T]](elements: List[T]): T =
    elements match {
      case List() => 
        throw new IllegalArgumentException("empty list!")
      case List(x) => x
      case x :: rest =>
        val maxRest = maxList(rest)  // (orderer) is implicit
        if (x > maxRest) x           // orderer(x) is implicit
        else maxRest
    }

视图界定与上界

注意这不同于上界表达的意思,上界T <: Ordered[T]的意思是:TOrdered[T]

相比之前用到的上界版本的maxListUpBound方法,唯一的区别就是上界符号与视图界定 符号的不同,但是我们的视图界定版本可以支持更多类型。

上下文界定

上下文界定的形式为T:M。意思是对于一个泛形类M,要存在一个从类型TM[T]的 隐式值。

如对于上下文界定:

class Pair[T : Ordering]

要有一个类型为Ordering[T]的隐式值,该隐式值可以被用在该类的方法中。因为在 Predef中已经有一个类型为Ordering[Int]的隐式值,所以可以这样定义Pair类:

class Pair[T : Ordering](val first: T, val second: T) {

	def smaller(implicit ord: Ordering[T]) =
		if (ord.compare(first, second) < 0) first else second

}

在进行new Pair(40, 2)操作时,编译器推断出类型为Pair[Int]。会把Predef中定义 的Ordering[Int]隐式值作为该类的一个字段传入到需要它的方法中。

也可以用Predef类的implicitly方法取得该值:

class Pair[T : Ordering](val first: T, val second: T) {

	def smaller = 
		if (implicitly[Ordering[T]].compare(first, second) < 0) first else second

}

这个implicitly函数在Predef.scala中是这样定义的:

def implicitly[T](implict e: T) = e

还有一个方法是利用Ordered特质中定义的从OrderingOrdered的隐式转换。在引入 了这个转换以后就可以使用关系操作符:

class Pair[T : Ordering](val first: T, val second: T) {

	def smaller = 
		import Ordered._;
		if (first < secone) first else second

}

重点是,只能有满足存在类型为Ordering[T]的隐式值条件,就可以随时实例化Pair[T] 。比如要实例化一个Pair[Point],就可以组织一个隐式的Ordering[Point]值:

implict object PointOrdering extends Ordering[Point] {
	def compare(a: Point, b: point) = ....
}

Manifest上下文界定

实例化一个Array[T]就需要一个manifest[T]对象。Array只是Scala提供的 一个类库,虚拟机中泛型相关的信息是被抹除的。

如果要实现一个泛型的方法来返回一个泛型的数组,就要转入一个Manifest对象来。由于 它是隐式参数,可以用上下文界定:

def makePair[T: Manifest](first: T, second: T) {
	val r = new Array[T](2); 
	r(0) = first; 
	r(1) = second; 
	r
}

对于调用makePair(4, 9),编译器会定位到隐式的Manifest[Int]并实际上调用的是 makePair(4, 9)(intmanifest)

这样在方法内创建数组的调用是new Array(2)(intManifest),返回基本类型的数组 int[2]

SAM(单个抽象方法)

Java中这种情况比Scala多:就是一个接口里只有一个抽象方法。

Scala里虽然可以把函数作为值,但是还是会有SAM的情况出现。比如说,Scala里明明可以 用函数值作为参数,但在实现一个GUI按钮的监听器为了兼容Java只能写一个SAM接口:

var counter = 0

val button = new JButton("Increment") // 记录按次数的按钮 
button.addActionListener(new ActionListener {
	override def actionPerformed(event: ActionEvent) {
		counter += 1
	}
})

解决方案是用隐式转换把一个函数转为ActionListener实例:

implicit def makeAction(action: (ActionEvent) => Unit) = new ActionListener {
	override def actionPerformed(event: ActionEvent) { action(event) }
}

现在调用时只要传入函数,接口实现类由隐式函数来实现:

button.addActionListener((event: ActionEvent) => counter += 1)

隐式操作的调试

隐式操作很强大,但也很难调试。这一节包含了一些技巧。

在REPL环境中:

  • :implicits查看Predef以外被引入的隐式成员。
  • :implicits -v查看全部。

对于编译器,可以检查指定的源代码文件使用了哪些隐式转换:

scalac -Xprint:typer MyProg.scala

这样会显示加入了隐式转换后的源代码。

没有对应的转换规则

在程序员认为应该隐式转换但编译器没有转换时,手动把转换调用写出来。这样如果有报错 就知道没有隐式转换的原因了。如下面的代码错把stringWrapper当成String转到 List而不是RandomAccessSeq

scala> val chars: List[Char] = "xyz"
<console>:7: error: type mismatch;
 found   : java.lang.String("xyz")
 required: List[Char]
       val chars: List[Char] = "xyz"
                               ^

被其他转换规则干扰

如果手动指定转换以后错误消失,那就很有可以是其他的转换规则覆盖了你想要的替换规则 。

  scala> val chars: List[Char] = stringWrapper("xyz")
  <console>:12: error: type mismatch;
   found   : java.lang.Object with RandomAccessSeq[Char]
   required: List[Char]
         val chars: List[Char] = stringWrapper("xyz")
                                 ^

编译器的-Xprint:typer显示隐式转换的信息。如下面的代码中:

  object Mocha extends Application {

    class PreferredDrink(val preference: String)

    implicit val pref = new PreferredDrink("mocha")

    def enjoy(name: String)(implicit drink: PreferredDrink) {
      print("Welcome, "+ name)
      print(". Enjoy a ")
      print(drink.preference)
      println("!")
    }

    enjoy("reader")
  }

最后一行的:

  enjoy("reader")

已经被扩展为了:

  Mocha.this.enjoy("reader")(Mocha.this.pref)

scala - Xprint:typer

  $ scalac -Xprint:typer mocha.scala
  [[syntax trees at end of typer]]// Scala source: mocha.scala
  package <empty> {
    final object Mocha extends java.lang.Object with Application
        with ScalaObject {

      // ...

      private[this] val pref: Mocha.PreferredDrink =
        new Mocha.this.PreferredDrink("mocha");
      implicit <stable> <accessor>
        def pref: Mocha.PreferredDrink = Mocha.this.pref;
      def enjoy(name: String)
          (implicit drink: Mocha.PreferredDrink): Unit = {
        scala.this.Predef.print("Welcome, ".+(name));
        scala.this.Predef.print(". Enjoy a ");
        scala.this.Predef.print(drink.preference);
        scala.this.Predef.println("!")
      };
      Mocha.this.enjoy("reader")(Mocha.this.pref)
    }
  }