1 隐式转换

隐式转换(也称为隐式视图),指把一种类型自动转换到另一种类型,以符合表达式的要求。


In [1]:
def foo(msg: String) = println(msg)


defined function foo

In [2]:
foo(5)


Compilation Failed
Main.scala:51: type mismatch;
 found   : Int(5)
 required: String
foo(5)
    ^

由于类型不匹配,出现编译失败的错误提示。我们需要进行隐式转换来完成调用。


In [3]:
implicit def intToString(x: Int) = x.toString


defined function intToString

In [4]:
foo(5)


5

1.1 以->方法为例


In [5]:
Map("one" -> 1, "two" -> 2)


res3: Map[String, Int] = Map("one" -> 1, "two" -> 2)

上面的方法调用了Map.apply方法,而该方法的输入时一组可变数量的pair对象:

def apply[A, B](elems: (A, B)*): Map[A, B]

这里并没有见到->方法的使用,所以其实->方法使用了隐式转换。

通过隐式转换,将任意两种类型值之间插入函数->,将表达式a -> b转换为元组(a, b)。

如果我们为所有可能作为元组首元素的类型都添加->方法肯定不现实。定义该方法的技巧是使用一个具有->方法的“封装”对象,Scala在Predef对象中定义了该对象:

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

我们可以依照上面的定义来构造Map对象:


In [6]:
Map(new ArrowAssoc("one") -> 1, new ArrowAssoc("two") -> 2)


res4: Map[String, Int] = Map("one" -> 1, "two" -> 2)

由于ArrowAssoc类被声明为implicit类,因此编译器将执行下列逻辑:

  1. 编译器发现我们试图对String对象执行->方法
  2. 有String未定义->方法,编译器将检查当前作用域中是否存在定义了该方法的隐式转换
  3. 编译器发现了ArrowAssoc类
  4. 编译器将创建ArrowAssoc对象,并向其传入one字符串
  5. 编译器将解析表达式中的-> 1部分代码,并确认了整个表达式的类型与Map.apply方法的预期类型相吻合,即两者均为pair实例

1.2 隐式转换的查询规则

能够执行隐式转换的只有两类:构造方法中只接受单一参数的类型或者是只接受单一参数的方法

从Scala 2.10以后,隐式方法变为Scala的可选特性,在使用时要通过import scala.language.implicitConversions开启这一特性,也可以使用全局的编译器选项-language: implicitConversions开启该特性。

编译器进行查找和使用隐式转换方法时的查询规则:

  • (1) 假如调用的对象和方法成功通过了组合类型检查,那么类型转换不会被执行
  • (2) 编译器只会考虑使用implicit关键字的类和方法
  • (3) 编译器只会考虑当前作用域内的隐式类、隐式方法,以及目标类型的伴生对象中定义的隐式方法
  • (4) 隐式方法无法串行处理,我们无法通过一个中间类型,使用串行的隐式方法将起始类型转换成目标类型。编译器执行隐式转换时只会考虑那些接受单一类型实例输入且返回目标类型实例的方法
  • (5) 假如当前适用多条隐式方法,那么将不会执行转换操作。编译器要求有且必须只有一条满足条件的隐式方法,以免产生二义性

1.3 构建字符串插入器的例子

Scala会提供一些内置的方法通过插入方式对字符串进行格式化,例如s"Hello, ${name}",Scala编译器会查找scala.StringContext的s方法,将其转换成StringContext("Hello, ", "").s(name)

传递给StringContext.apply方法的参数是${...}表达式中抽取出的各个部分,传递给s从参数则是抽取后的表达式。

下面,我们通过隐式转换为StringContext添加新的方法,对其进行扩展,定义新的插入器。编写一个将简单JSON字符串转换为scala.util.parsing.JSONObject对象的插入器(该插入器功能简单,只处理扁平的JSON表达式)。


In [7]:
// Scala 2.11中,json包被放置在一个独立的解析器-组合器库中,这里需要加载jar包
load.jar("D:\\scala\\lib\\scala-parser-combinators_2.11-1.0.4.jar")




In [8]:
import scala.util.parsing.json._

object Interpolators {
    implicit class jsonForStringContext(val sc: StringContext) { //为了限制隐式类的作用域,必须在对象内定义隐式类
        def json(values: Any*): JSONObject = { //该方法输入字符串中嵌套的参数,返回JSONObject对象
            val keyRE = """^[\s{,]*(\S+):\s*""".r //抽取key名称的正则表达式
            val keys = sc.parts map { 
                case keyRE(key) => key
                case str => str
            }
            val kvs = keys zip values // 将key和value压缩成键值对集合
            JSONObject(kvs.toMap) //使用键值对构成Map对象,并构造JSONObject对象
        }
    }
}


import scala.util.parsing.json._
defined object Interpolators

In [9]:
import Interpolators._

val name = "Dean Wampler"
val book = "Programming Scala, Second Edition"
val jsonobj = json"{name: $name, book: $book}"
println(jsonobj)


{"name" : "Dean Wampler", "book" : "Programming Scala, Second Edition"}
import Interpolators._
name: String = "Dean Wampler"
book: String = "Programming Scala, Second Edition"
jsonobj: JSONObject = JSONObject(
  Map("name" -> Dean Wampler, "book" -> Programming Scala, Second Edition)
)