《深入理解 Scala》第五章 — 利用隐式转换编写更有表达力

Scala 的隐式转换系统定义了一套定义良好的查找机制,让编译器能够调整代码。Scala 编译器可以推导下面两种情况:

  • 缺少参数的方法调用或构造器调用;
  • 缺少了的从一种类型到另一种类型的转换。(是指调用某个对象上不存在的方法时,隐式转换自动把对象转换成有该方法的对象)

介绍隐式转换系统

implicit 关键字可通过两种方式使用:1、方法或变量定义;2、方法参数列表。如果关键字用在变量或方法上,等于告诉编译器在隐式解析(implicit resolution)的过程中可以使用这些方法和变量。

隐式解析是指编译器发现代码里的缺少部分信息,而去查找缺少信息的过程。

implicit 关键字用在方法参数列表的开头,是告诉编译器应当通过隐式解析来确定所有的参数值。

scala> def findAnInt(implicit x: Int) = x
findAnInt: (implicit x: Int)Int

scala> findAnInt
<console>:9: error: could not find implicit value for parameter x: Int
              findAnInt
              ^

scala> implicit val test = 5
test: Int = 5

scala> findAnInt
res1: Int = 5

scala> def findTwoInt(implicit a: Int, b: Int) = a + b
findTwoInt: (implicit a: Int, implicit b: Int)Int

scala> findTwoInt  // implicit 作用于整个参数列表
res2: Int = 10

scala> implicit val test2 = 5
test2: Int = 5

scala> findTwoInt
<console>:11: error: ambiguous implicit values:
 both value test of type => Int
 and value test2 of type => Int
 match expected type Int
              findTwoInt
              ^

scala> findTwoInt(2, 5)  // 隐式的方法参数仍然能够显式给出
res4: Int = 7

隐式的方法参数仍然能够显式给出。

标识符

Scala 定义了术语 entity 来代表类型、值、方法或类,这些是构建程序的基本要素,通过标识符或名字来引用它们,这在 Scala 里称为绑定。

import 语句可以在源代码的任何位置使用,而且只会在 import 语句所在的局部作用域创建绑定。

Scala 的导入语句可以给导入的实体赋予任意名称,使用语法:{OriginBinding => newBinding}。导入语句还可以用来改变包本身的名字,如: import java.{io => jio}

绑定允许我们在特定的作用域里给它们命名。

作用域和绑定

作用域是一个词法边界,而绑定位于其中。作用域可以是任何东西,从类的实体(body of a class)到方法体到匿名代码块都可以是作用域。一个通用规则是当用 {} 的时候就创建了一个作用域。作用域是可以嵌套的。

同作用域内的高优先级绑定遮挡(shadow)低优先级绑定,高优先级或同优先级绑定遮挡外部作用域的绑定。

Scala 对绑定定义来如下优先级:

  1. 本地的、继承的,或者通过定义所在的源代码文件的 package 语句所引入的定义和声明具有最高优先级。
  2. 显式导入的具有次高优先级。
  3. 通配符导入的具有更次一级的优先级。
  4. 由非定义所在的源文件里的 package 语句引入的定义优先级最低。

通配导入的优先级高于同包不同源文件的绑定优先级。

隐式解析

Scala 语言规范定义以下两种查找标记为 implicit 的实体的规则:

  • 隐式实体在查找发生的地点可见,并且没有任何前缀,比如不能是 foo.x,只能是 x
  • 如果按照前一条规则没有找到隐式实体,那么会在隐式 参数的类型 的隐式作用域里包含的所有隐式实体里查找。

对于规则二,编译器会在其目标类型的 隐式作用域 里面的任何对象里查找定义于其中的隐式实体。一个类型的隐式作用域是指与该类型相关联的全部伴模块。如果一组类型与 T 相关联,在隐式解析的时候所有这些类型的伴生对象都会被搜索。

Scala 语言规范里定义的”关联“是指类型 T 的某部分的任何一个基类。类型 T 的某部分定义如下:

  • T 的全部子类型(subtype)都是 T 的部分。如果 T 定义为 A with B with C,那么 A、B、C 都是 T 的部分,在 T 的隐式解析过程中,它们的伴生对象会被搜索。
  • 如果 T 是参数化类型,那么所有类型参数和类型参数的部分都算作 T 的部分。比如对 List[String] 的隐式搜索会搜索 List 的伴生对象和 String 的伴生对象。
  • 如果 T 是一个单例类型 p.T,那么类型 p 的部分也包含在 T 的部分里。也就是说,如果类型 T 位于某个对象内,那么那个对象也会被搜索。
  • 如果 T 是个类型注入 S#T,那么 S 的部分也包含在 T 的部分里。也就是说,如果类型 T 位于某个类型或特质里,那么那个类型或特质的伴生对象也会被搜索。

通过类型参数获得隐式作用域

object holder {
    trait Foo
    object Foo {
        implicit val list = List(new Foo{})
    }
}


println(implicitly[List[holder.Foo]])

implicitly 的函数定义为 def implicitly[T](implicit arg: T): T,用于在当前作用域里查找指定类型。

类型特质又称为 类型类(type class):用类型参数来描述通用接口,以便能为任意类型创建(接口的)实现。

包对象(package object)就是用 package object 语法定义的对象,在 Scala 里,一般约定把包对象放在与包名对应的目录下的 package.scala 文件里。

任何定义在某个包里的类都算作该包的嵌套类。任何定义在包对象里的隐式绑定对于定义在该包里的所有类型都可见。

隐式视图:强化已存在的类

隐式视图是指一种把类型自动转换到另一种类型,以符合表达式的要求。隐式视图定义一般用如下形式:implicit def <myConversionName> (<argumentName>: OriginalType): ViewType 。在需要的时候,如果隐式作用域里存在这个定义,它会隐式地把 OriginalType 类型的值转换为 ViewType 类型的值。

隐式视图用在以下两种场合:

  • 如果表达式不符合编译器要求的类型,编译器会查找能使之符合类型要求的隐式视图。
  • 给定一个 e.t ,这里的选择是指成员变量访问,如果 e 的类型里并没有成员 t,则编译器会查找能应用到 e 类型并且返回类型包含成员变量 t 的隐式视图。

隐式视图所使用的作用域与隐式参数相同。但是当编译器查找关联类型时,会使用转换的”源类型“而不是”目标类型“。

隐式视图这种机制使得判断同个类型的多个隐式视图之间是否存在命名冲突变得非常困难,也可能会有 HotSpot 优化器无法改善性能开销。

隐式参数结合默认参数

当未提供参数,且通过隐式解析无法找到隐式值的时候,编译器就会使用默认参数。

限制隐式系统的作用域

在应用隐式系统时最重要的一点是确保程序员理解一块代码里发生了什么,要做到这点就必须缩小程序员查找可用隐式值的时候需要看的代码范围。

隐式绑定可能所处的位置:

  • 所有关联的伴生对象,包括包对象;
  • scala.Predef 对象;
  • 作用域里所有导入语句。

伴生对象和包对象应当视作一个类的 API 的一部分。

在定义期望被显式导入的隐式视图或隐式参数时,需要确保以下几点:

  • 隐式视图或参数与其他隐式值没有冲突;
  • 隐式视图或参数的名字不和 scala.Predef 对象里的任何东西冲突。
  • 隐式视图或参数是”可发现的“,是指库或模块的用户能够找到隐式转换的位置和判等其用法。

在 Scala 社区中,通常的做法是把可导入隐式转换限制在以下两个位置之一:

  • 包对象;
  • 带 implicits 后缀的单例对象。

小结

Scala 支持两种形式的隐式转换:隐式值和隐式视图。隐式值可以用于给方法提供参数;隐式视图可以用于类型之间转换,或使针对某类型的方法调用成功。隐式值和隐式视图都使用相同的隐式解析机制。

隐式解析采用一种两阶段过程:第一阶段在当前作用域里查找不带前缀的隐式转换;第二阶段在关联对象的伴生对象里进行查找。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据