《Scala 程序设计》学习笔记 Chapter 5:隐式详解

  • 除了在 Predef 对象中自动加载的那些隐式对象外,其他在源码中出现的隐式对象均不是本地对象。[P112]

隐式参数

  • 在方法中使用 implicit 关键字标记隐式参数。当调用方法时未输入隐式参数时,如果代码所处的作用域存在类型兼容值,类型兼容值会从作用域中调出并被使用,反之,系统会抛出错误。[P112]

    def calcTax(amount: Float)(implicit rate: Float): Float = amount * rate
    
    implicit val currentTaxRate = 0.08F
    ...
    val tax = calcTax(50000F)
    
  • 可以使用隐式方法动态计算隐式参数的值。隐式方法声明在隐式对象中,隐式对象不具备任何参数,除非该参数同样被标示为隐式参数。[P113]

    case class ComplicatedSalesTaxData(
        baseRate: Float,
        isTaxHoliday: Boolean,
        storeId: Int)
    object ComplicatedSalesTax {
        private def extraTexRateForStore(id: Int): Float = {
            ...
        }
        implicit def rate(implicit cstd: ComplicatedSalesTaxData): Float = {
            if (cstd.isTaxHoliday) 0.0F
            else cstd.baseRate + extraTaxRateForStore(cstd.storeId)
        }
    }
    
    import ComplicatedSalesTax.rate
    implicit val myStore = ComplicatedSalesTaxData(0.06F, false, 1010)
    println(s"Tax on $amount = ${calcTax(amount)}")
    
  • 可以定义包含隐式参数的隐式方法。[P113]

  • 调用 implicitly 方法:[P114 - 原书有错误]

    • Predef 对象中定义了一个名为 implicitly 的方法。如果将 implicitly 方法与附加类型签名( type signature addition )相结合,便能以一种有用且快捷的方式定义一个接收参数化类型隐式参数的函数。

      import math.Ordering
      case class MyList[A](list: List[A]) {
          def sortBy1[B](f: A => B)(implicit ord: Ordering[B]): List[A] = {
              list.sortBy(f)(ord)
          }
          def sortBy2[B: Ordering](f: A => B): List[A] = { // [B: Ordering] 的作用是限制 B 为 Ordering 的子类。
              list.sortBy(f)(implicitly[Ordering[B]])
          }
      }
      
      • 在上述代码中, sortBy1 接收一个额外的类型为 Ordering[B] 的隐式值作为其输入。调用 sortBy1 方法时,在当前作用域一定存在某一 Ordering[B] 的对象实例,该实例清楚地知道如何对我们所需要的 B 类型对象进行排序。
      • Scala 为这种普遍的操作提供了一种简化的方式,正如 sortBy2 使用的语法那样:类型参数 B 被称为上下文定界( context bound ),它暗指第二个参数列表(隐式参数列表)将接受 Ordering[B] 实例。implicitly 方法会对传给函数的所有标记为隐式参数的实例进行解析。
    • 当需要类型为参数化类型的隐式参数时,当类型参数属于当前作用域的其他一些类型时(例如 [B: Ordering] 代表了类型为 Ordering[B] 的隐式参数 ),可以将上下文定界与 implicitly 方法结合起来,以简洁的方式解决这个问题。

隐式参数的适用场景

  • 通过隐式参数实现的常见习语( idiom ):[P115]
    • 能够消除样板代码
    • 通过引入约束来减少 bug 数量以及使用参数化类型对某些方法允许的输入参数类型进行限定。

执行上下文

  • 建议使用隐式参数传入执行上下文。另外,编写事务、数据库连接、线程池以及用户会话时隐式参数上下文也同样适合使用。[P115]

功能控制

  • 用隐含参数控制系统功能 [P115 - 116]
    • 引入授权令牌,控制某些特定的 API 操作只能供某些用户调用,或者决定数据可见性等。

    • 可以使用隐式用户会话参数包含这类令牌信息。

      def createMenu(implicit session: Session): Menu = {
          val defaultItems = List(helpItem, searchItem)
          val accountItems = 
              if (session.loggedin()) List(viewAccountItem, editAccountItem)
              else List(loginItem)
          Menu(defaultItems ++ accountItems)
      }
      

限定可用实例

  • 对具有参数化类型方法中的类型参数进行限定,使该参数只接受某些类型的输入。[P116]
  • 应用 Scala API :[P116 - 117]
    • 将一个“构造器”( builder )作为隐式参数传入到 map 方法中。该构造器知道如何构造一个同种类型的新容器。参考 TraversableLike

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

      map 操作符可以输出的集合类型是由当前存在的对应的 CanBuildFrom 构造器 实例所决定的,而这些构造器在当前作用域被声明为隐式对象。

隐式证据

  • 只需要限定允许的类型,不需要提供额外的处理;这些类型无需继承某一个共有的超类。[P120 - 121]
  • 一个例子:TraversableOnce [P121]
    • <:< 类型(定义在 Predef 中):用于限定类型参数。
      • <:<[A, B] 等价于 A <:< B
  • Predef 还定义了一个名为 =:= 的“证据”类型,用于证明两个类型之间的等价关系。[P122]

绕开类型擦除带来的限制

  • 通过使用隐式对象提供的“证据”证明输入满足某些特定的类型约束。[P122]

    object M {
        implicit object IntMarker
        implicit object StringMarker
        
        def m(seq: Seq[Int])(implicit i: IntMarker.type): Unit = {
            println(s"Seq[Int]: $seq")
        }
        def m(seq: Seq[String])(implicit s: StringMarker.type): Unit = {
            println(s"Seq[String]: $seq")
        }
    }
    

    (不推荐使用常用类型的隐式值,因为常用类型可能会在多处定义其对应的隐式对象。)

改善报错信息

  • 查看一下 scala.annotation.implicitNotFound 的 annotation 。[P124]

虚类型

  • 定义好的、没有任何实例的类型被称为虚类型。(只关心类型本身,而不会使用类型中的任何实例。)�[P124 - 125]

  • 可插入字符串:println(f"args: ${args}%.2f" [P126]

  • 管道操作符:|> [P127]

    val pay1 = Payroll start e
    val pay2 = Payroll minus401k pay1
    val pay3 = Payroll minusInsurance pay2
    val pay4 = Payroll minusTax pay3
    val pay = Payroll minusFinalDeductions pay4
    

    等价于

    import Payroll._
    val pay = start(e) |>
        minus401k |>
        minusInsurance |>
        minusTax |>
        minusFinalDeductions
    

    其实质是进行了一步转化:pay1 |> Payroll.minus401k => Payroll.minus401k(pay1)

隐式转换

  • Scala 并不知道诸如 a -> b 这样的语法的意义是什么,它实际上是运用了方法 -> 和一个特殊的 Scala 特性 —— 隐式转换。同时,由于 -> 并不是元组的字面量语法,因此 Scala 必须通过某些方式将该表达式转化为元组 (a, b) 。[P128 - 129]

    // In Predef
    implicit final class ArrowAssoc[A](val self: A) {
        def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y)
    }
    
  • 观察编译器行为(以 "one" -> "1" 为例)�:[P129]

    • 编译器发现我们试图对 String 对象执行 -> 方法;
    • 由于 String 未定义 -> 方法,编译器将检查当前作用域中是否存在定义了该方法的隐式转换;
    • 编译器发现了 ArrowAssoc 类,创建其对象,并向其传入 "one" ;
    • 编译器解析表达式中的 -> 1 部分代码并执行。
  • 如果希望执行隐式转换,那么在声明时必须使用 implicit 关键字,能够执行隐式转换的无外乎两类:构造方法中只接受单一参数的类型或者是只接受单一参数的方法。[P129]

  • 从 Scala 2.10 开始,隐式方法变成了 Scala 的可选特性。假如希望使用该特性,需要 import scala.language.implicitConversions ;或者使用全局编译器选项 -language:implicitConversions 。[P130]

  • 编译器进行查找和使用转换方法时的查询规则:[P130]

    • 假如调用的对象和方法成功通过了组合类型检查,那么类型转换不会被执行。
    • 编译器只会考虑使用了 implicit 关键字的类和方法。
    • 编译器只考虑当前作用域内的隐式类,隐式方法,以及目标类型的伴生对象中定义的隐式方法。
    • 假如当前适用多条转换方法,那么将不会执行转换操作。编译器要求有且必须只有一条满足条件的隐式方法,以免产生二义性。

构建独有的字符串插入器

  • 对于字符串,当编译器看到形如 x"..." 这样的表达式时,它会查找 scala.StringContext 中定义的 x 方法。s"Hello, ${name}" 可以被转换为 StringContext("Hello, ", "").s(name) 。[�P132]

  • 我们可以通过使用隐式转换为 StringContext 添加新方法,对其进行“扩展”。[P132 - 133]

  • 集合中的 zip 方法能很方便的将两个集合中的值缝合在一起:[P133]

    val keys = List("a", "b", "C")
    val values = List(1, 2, 3)
    val keysValues = keys zip values 
    // List((a, 1), (b, 2), (c, 3))
    

表达式问题

  • 在不修改源代码的情况下扩展模块的期望被称为表达式问题( expression problem )。[P134]
  • 使用继承遇到的问题:不同子类需要的功能可能并不相同,父类可能需要定义许多对于某一个子类多余的功能。[P134]
  • 单一职责原则( single responsibility principle )[P134]
  • 元编程( metaprogramming ):允许用户在元编程运行环境下,不修改源代码就可以修改类。[P134]
  • 通过 Scala 隐式转换,我们可以通过静态类型实现元编程的方法,我们称之为类型类( type class )。[P134]

类型类模式

  • Scala 的 case class 使用的语法比 Java 的 class 要有用得多。通过使用隐式转换,我们可以为任何类型添加 toJSONtoXML 方法。如果对象中未定义 toString 方法,我们也能通过隐式转换定义该方法。[P135]
  • 代码示例见书 P135 - 136
  • Scala 不允许同时使用 implicitcase ,因为 case class 不会执行通过隐式所自动生成的额外代码。[P136]
  • 扩展方法( extend method )是类型类的另一用途。[P136]
  • 类型类提供的多态叫特设多态( ad hoc polymorphism ),其并未绑定类型系统。 [P137]
  • 三种多态:子类型多态 / 特设多态 / 参数化多态 [P137]

隐式所导致的技术问题

  • 增加代码量。[P137]
  • 造成额外的运行开销:封装类型会引入额外的中间层。[P137]
  • 当隐式特征与其他 Scala 特征,尤其是子类型特征发生交集时,会产生一些技术问题。[P137 - 138]
  • 如何避免:[P138]
    • 无论何时都要为隐式转换方法指定返回类型。否则,类型推导推断出的返回类型可能会导致预料之外的结果。
    • 避免使用编译器提供的转换。

隐式解析规则

  • Scala 会解析无须输入前缀路径的类型兼容隐式值。(在相同代码块 / 相同类型 / 相同作用域 / 伴生对象中)[P139]
  • Scala 会解析那些导入到当前作用域的隐式值,其优先级高于已经在当前作用域的隐式值。[P139]
  • Scala 会自动选择类型匹配度最高的隐式。如果引发歧义,编译错误会被触发。[P139]
  • Scala 库中定义的隐式常常会被编译器自动加载到作用域中。[P139]

Scala 内置的各种隐式

[P139 - 146]

合理使用隐式

  • 可以考虑将隐式值全部放到名为 implicits 的特殊包或对象中。[P146]

你可能感兴趣的