这几天遇到了一个有关泛型的 Scala 试题,题目是这样的:
class Comp[T] (val p1:T, val p2:T) {
def getLarger = ???
def getSmaller = ???
}
要求实现这两个函数,我当场被震惊到了,这也太简单粗暴了。这道题目可以作为一个有关泛型的经典的反面教才来分析一下,看看我们能从中学到哪些东西。
我的第一反应是出题者应该是一个传统的 Javaer,他应该是受到 Java 的 Comparable 的启发,那么我们就来看一下 Java 的 Comparable 是怎么定义的:
public interface Comparable<T> {
public int compareTo(T o);
}
首先,Java 的 Comparable 是一个 interface 而试题是一个 class。Java interface 中的泛型参数通常用于定义通用(generic)泛型,”通用”意味着它的适用面是无限的,也就是说它不限定 Comparable 只适用于数值或少数可比较的类型。作为一个 interface,这种适用性通常是没问题的,因为它只是一个接口,只有在实现该接口时我们才需要决定它究竟要适配哪些具体类型。但是如果将类型参数作用于 class,那么就不同了,因为 class 的泛化属于子类型(subtype) 泛化,这是运行时(runtime)泛化技术。这种技术在 Java 中使用的非常普遍,也就是考题的形态。
和 interface 不同,子类型泛化无法被静态编译,编译器无法在编译期间预知它将会被作用于哪些类型,所以它是不安全的。可以想象,因为“业务”的特点决定了”通用”不会真的有无限的匹配需求,所以通常问题在开发初期被有限的需求掩盖了,造成了以后可扩展性的问题,一旦业务的比较范围扩大到了之前没能兼容的类型时,程序就出现了 bug。而在考题中出现这种定义尤其致命,因为你不可能在没有限定条件下枚举所有可能的类型逐个实现它们的比较,并且在多数情况下很可能两个 Object 根本就不能比较,那就真的成了“无限通用”了,因此这道题目脱离了上下文根本就是无解的。
我们分析了为什么这道题作为通用泛型
来考虑是无解的,顺带提到了通用泛型的缺点。与之对应的是泛函编程使用了特设(ad-hoc)泛型
来弥补这个缺陷。“特设(ad-hoc)泛型”顾名思义,是与特定类型对应的泛化技术,所以它具有有限的对应数量,在 Scala 中它通过 typeclass 来实现。
typeclass 的声明很简单:
trait Comparable[T] {
def getLarger(left: T, right:T):T
}
这和 Comparable interface 非常类似,甚至你可以将它依旧看成是通用泛型,但是 Scala 通过隐式可以将它视为特设泛型,它是怎么做到的呢?有两种方式:隐式参数
和 context bound(上下文限定)
。以 context bound 为例,我们重新来声明这个 Comp 类并实现它:
case class Comp[T: Comparable] ( p1:T, p2:T) {
def getLarger:T = {
val comp = implicitly[Comparable[T]]
comp.getLarger(p1, p2)
}
}
你会发现这个类的定义和考题非常相似,除了类型签名略有不同外。该怎么理解 [T: Comparable]
呢?这恰是特设泛型的匹配宣告,它的意识是:当该 Comp 被实例化的时候,上下文中必须存在一个特定于 Comparable[T] 的实例。注意它与 class Comp[T]
语意上的区别,后者并没有表达出对 Comparable[T] 的渴望。因为后者,也就是纯通用泛型在编译期不考虑特设匹配,因为它的适用类型是不限定的。换一句话说,这个 Comparable
就充当了限定符(bound)
的作用,有了这个 bound ,我们就”有法可依”,基本上我们可以预期这个新的 Comp 需要的是 Comparable 泛型的支持,也就是将 Comparable
视为特设泛型的依据,这就是 bound 的意义。接下来就是要解决 Comparable 的类型参数 T 了,这时候另一个关键特性类型推断
就派上了用场,让我来生成一个 Comp 的实例:
Comp(1, 2).getLarger
以上代码在编译的时候,编译器会根据参数 1和 2 推断出 Comparable 的参数是 Int 类型,因此编译器推断出它的具体限定类是 Comparable[Int],这就是特设匹配的具体匹配条件,在Comp 的这个实例中,它被限定在了Comparable[Int] 这个特定的泛型上。
现在既然编译器知道了我们需要 Comparable[Int],那么接下来就好办了,它只需要在上下文中寻找是否存在这个 bound 的实例,于是我们只需要再次通过隐式(implicit)
为它声明一个特设实例:
implicit val compInt: Comparable[Int] = (left: Int, right: Int) => {
if (left > right) left else right
}
implicit
关键词是必不可少的,它激活了 Scala 的依赖注入机制。由于这个 Comparable[Int] 实例是为 Int 特设的,因此它必定满足针对 Int 的 getLarger 方法。并且,编译器在编译期间就获得了全部信息并以此找到了支持代码,所以这样编译出来的代码可以放心地交由运行期去执行。假设在未来,我们在业务中提出了对 String 类型的支持,那么当我们输入以下代码时:
Comp("a", "b").getLarger
编译器同样会去寻找是否存在 Comparable[String]
特设实例,如果没有找到那么会拒绝编译,于是我们就会在最终交付修改前得到一个错误报告,它为我们的功能扩展提供了安全指导,相比于无法断定是否安全的 T
,显然这样的代码安全了许多。
以上整个过程听起来比较抽象,让我们用一个更形象的例子来类比。假设我们生活在一个纷繁复杂的世界中,现在我们要在人群中挑选出一个最强壮者去丛林中执行一项冒险任务,那么我们怎么比较每个人的能力呢?于是我们设计出竞技场(Arena)来作为竞选的场合,在这个场合中我们通过 match (比赛)方法,制定了适合人类比赛的规则,那么当一个特定的 Arena[Human] 出现时,它代表适用于人类竞技的 bound
;同时,我们还设计出适合猎狗的 Arena[Hound],它也具有 match 方法,但是具体规则和人类的不一样。以此类推,当科技发展了,有一天我们发明出了枪械,这时候我们的任务就会发出抱怨它无法找到合适的竞技场来比较新的枪械,于是我们不得不设计新的 Arena[Gun] 来满足新的需求……很显然,因为不同的 Arena 的存在,我们避免了将人类、猎狗、和未来出现的枪械同时应用于同一规则的风险,这就是特设泛型为通用泛型带来的显著改变。
好了,理解了 [T: Comparable] 和 [T] 参数的区别,我们再回来思考这道题目背后的含义就会恍然大悟,它实际上混淆了特设泛型和通用泛型,给应试者发出了完全错误的信息。因为在 Scala 泛型编程的世界中,特设泛型是标配,而通用泛型是不被推荐的,由此可见这道考题如果作为通用泛型,它是无解的(理由参考开头),而作为特设泛型,它的类型签名又是错误的,因此这是一道彻头彻尾错误的考题,不过虽然如此,通过分析它我们还是能够学到不少东西,并非所有的错误都没有营养。