[译] Scala 类型的类型(二)

本文由 Yison 发表在 ScalaCool 团队博客。算法

上一篇

Scala 类型的类型(一)安全

目录

6. 一个单例对象的类型

Scala 的单例对象( object) 是经过 class 实现的(显而后者就像 JVM 的基础构件)。然而你也会发现咱们并不能像一个简单的类同样,轻松地得到一个单例对象的类型……app

我经常疑惑该如何传一个单例对象给一个方法,对此我本身也很是惊讶。个人意思是指 obj: ExampleObj 是无效的,由于这种状况 ExampleObj 已经指向了实例,因此它有个 type 的成员,咱们能够靠它解决问题。ide

下面的代码解释了大概的方法:函数

object ExampleObj

def takeAnObject(obj: ExampleObj.type) = {}

takeAnObject(ExampleObj)复制代码

7. Scala 中的型变

术语 翻译
Variance 型变
Invariant 不变
Covariant 协变
Contravariant 逆变
Immutable 不可变的
Mutable 可变的

上述表格由译者自主添加,避免形成误解。性能

型变,一般能够解释成类型之间依靠彼此的「兼容性」,造成一种继承的关系。最多见的例子就是当你要处理「容器」或「函数」的时候,有时就必需要处理型变(极其的常见!)。ui

Scala 跟 Java 一个重大的差别,就是它的「容器类型」默认是不变的!也就是说,若是你有一个定义为 Box[A] 的容器,而后在使用的时候将其中的类型参数 A 替换成 Fruit,以后你就不能插入一个 Apple 类型(Fruit 子类)的值。spa

Scala 中的型变经过在「类型参数」前使用 +- 符号来定义。.net

参见:www.slideshare.net/dgalichet/d…scala

概念 描述 Scala 语法
不变 C[T'] 与 C[T] 是不相干的 C[T]
协变 C[T'] 是 C[T] 的子类 C[+T]
逆变 C[T] 是 C[T'] 的子类 C [-T]

以上的表格较抽象地罗列了全部咱们须要担忧的型变状况。也许你还在疑惑何时须要关心这些,事实上当你每次处理 collection 的时候就遇到了 — 你必须思考「这是一个协变吗?」。

大部分不可变的 collection 是协变的,而大多数可变的 collection 是不变的。

在 Scala 中至少有两个不错并很直观的例子。一个是 collection,咱们将使用 List[+A] 来举例;另外一个就是「函数」。

当咱们讨论 Scala 中的 List 时,一般指的是 scala.collection.immutable.List[+A] ,它是不可变的,且是协变的。让咱们看看这与「构建一个包含不一样类型成员的 list」有什么联系。

class Fruit
case class Apple() extends Fruit
case class Orange() extends Fruit

val l1: List[Apple] = Apple() :: Nil
val l2: List[Fruit] = Orange() :: l1

// and also, it's safe to prepend with "anything",
// as we're building a new list - not modifying the previous instance

val l3: List[AnyRef] = "" :: l2复制代码

值得一提的是,当存在不可变的 collection 时,协变是安全的。若是 collection 可变,则不成立。这里典型的例子是 Array[T],它是不变的。下面就来看看「不变」对咱们来讲意味着什么,以及它是如何让咱们免于错误:

// won't compile
val a: Array[Any] = Array[Int](1, 2, 3)复制代码

由于 Array 的不变,这样一个赋值操做就不会被编译。假使这个赋值被经过了,咱们就陷入麻烦了。咱们会写出这样子的代码:a(0) = "" // ArrayStoreException!,这将引起可怕的 ArrayStoreException 失败。

咱们曾说过在 Scala 中「大部分」不可变的 collection 是协变的。若是你想知道一个「相反是不变」的特例,它是 Set[A]

7.1 特质(trait)— 能够带有实现的接口

首先,让咱们看看关于「特质」最简单的一个问题:咱们如何将多个特质混入到一个类型中,就像若是你来自 Java,会把这叫作「接口实现」同样:

class Base { def b = "" }
trait Cool { def c = "" }
trait Awesome { def a ="" }

class BA extends Base with Awesome
class BC extends Base with Cool

// as you might expect, you can upcast these instances into any of the traits they've mixed-in
val ba: BA = new BA
val bc: Base with Cool = new BC

val b1: Base = ba
val b2: Base = bc

ba.a
bc.c
b1.b复制代码

目前而言,你应该都比较好理解。如今让咱们来讨论下「钻石问题」,熟悉 C++ 的读者可能一直在期待吧。钻石问题(菱形继承问题)主要描述的是在「多重继承」的状况下,咱们「没法明确想要继承什么」的处境。若是你认为特质也相似多重继承同样,下图揭示了这个问题。

7.2 类型线性化 VS 钻石问题


Diamond Inheritance

要说明「钻石问题」,咱们只要有一个 BC 中的覆盖实现就好了。当咱们调用 D 中的 common 方法的时候,产生了歧义 — 咱们究竟是继承了 B 仍是 C 的方法?在 Scala 里,若是仅仅只有一个覆盖方法的状况下,这个问题很简单 — 就是这个覆盖方法。但假使是更复杂的状况呢?让咱们来研究一下:

  • class A 定义了方法 common ,返回 a
  • trait B 覆盖 common ,返回 b
  • trait C 覆盖 common ,返回 c
  • class D 同时继承 BC ;
  • 请问 D 继承了谁的 common ?究竟是 C ,仍是 B

这种歧义是每一个「多重继承」机制的痛点之一,Scala 经过一种称为「类型线性化」的手段来解决这个问题。
换句话说,在一个钻石类结构中,咱们老是能够明确地决定在 D 中要调用的 common 方法。咱们先来看看下面这段代码,再来讨论线性化:

trait A { def common = "A" }

trait B extends A { override def common = "B" }
trait C extends A { override def common = "C" }

class D1 extends B with C
class D2 extends C with B复制代码

结果以下:

(new D1).common == "C"

(new D2).common == "B"复制代码

之因此会这样,是因为 Scala 在这里为咱们采用了类型线性化规则。算法以下:

  • 首先构建一个类型列表,第一个元素就是咱们首要线性化的类型(译者注:刚开始列表是空的);
  • 将每一个超类型递归地展开,而后把全部的类型放入到此列表中(这应该是扁平的,而不是嵌套的);
  • 删除结果列表的重复项,从左到右对列表进行扫描,删除已经存在的类型;
  • 操做完成。

让咱们将这个算法人肉地应用到咱们的钻石实例当中,来验证为何 D1 extends B with C(以及 D2 extends C with B
会产生那样的结果:

// start with D1:
B with C with <D1>

// expand all the types until you rach Any for all of them:
(Any with AnyRef with A with B) with (Any with AnyRef with A with C) with <D1>

// remove duplicates by removing "already seen" types, when moving left-to-right:
(Any with AnyRef with A with B) with (                            C) with <D1>

// write the resulting type nicely:
Any with AnyRef with A with B with C with <D1>复制代码

显然,当咱们调用 common 方法时,能够很容易决定咱们想要调用的版本:咱们只需看一下线性化的类型,并尝试从右边的线性化类型结果中解析出来。在 D1 的例子中,实现 common 的特质是 C,因此它覆盖了 B 提供的实现。在 D1 中调用 common 的结果将是 "c"

你能够认真考虑在 D2 上尝试这种方法 — 若是你运行代码,它应该会前后对 CB 进行线性化,从而产生一个为 "b" 的结果。而且,你也能够简单地利用「最右取胜」的原则来简化线性化规则的理解,但尽管这个有用,却并无展示整个算法的全貌。

值得一提的是,咱们也能够经过这种技术来获知「谁是咱们的超类?」。如同在线性化类型中「朝左看」同样简单,你就能知道任何类的超类是谁。因此在咱们的 D1 例子中,C 的超类是 B

8. Refined Types (refinements)

Refinements 能够很简单地理解为「匿名的子类化」。因此在源代码中,能够是相似这个样子:

class Entity

trait Persister {
  def doPersist(e: Entity) = {
    e.persistForReal()
  }
}

// our refined instance (and type):
val refinedMockPersister = new Persister {
  override def doPersist(e: Entity) = ()
}复制代码

9. 包对象

Scala 在 2.8 版本中引入了包对象(Package Object),这自己并无真的拓展了类型系统。但包对象们提供了一种至关有用的模式,能够一块儿引入一堆东西,此外编译器也会在它们那寻找隐式的值。

声明一个包对象很简单,只要一块儿使用 packageobject 关键字就好了,就像这样子:

// src/main/scala/com/garden/apples/package.scala

package com.garden

package object apples extends RedApples with GreenApples {
  val redApples = List(red1, red2)
  val greenApples = List(green1, green2)
}

trait RedApples {
  val red1, red2 = "red"
}

trait GreenApples {
  val green1, green2 = "green"
}复制代码

约定上,咱们将包对象们定义在 package.scala 中,而后放置到目标 package 下。你能够经过调查上述例子的文件源路径以及 package 来加深理解。

从使用方面来讲,这带来了真正的好处。由于当你引入包的时候,你也随之引入了在包中定义的全部状态:

import com.garden.apples._

redApples foreach println复制代码

10. 类型别名

类型别名(Type Alias)并非另外一种类型,而是一种咱们提升代码可读性的技巧。

type User = String
type Age = Int

val data:  Map[User, Age] =  Map.empty复制代码

经过这样的技巧,Map 的定义一会儿变得很清晰。若是咱们仅仅只使用一个 Sting => Int 的 map,代码的可读性就不那么好了。虽然咱们仍旧能够坚持使用咱们的原始类型(也许是出于如性能方面的考虑),但使用别名能让这个类后续的读者更容易理解。

注意,当你要为一个类建立别名的时候,并不会为它的伴生对象也创建别名。举个例子,假使你定义了 case class Person(name: String) 以及一个别名 type User = Person,调用 User("John") 就会出错。由于 Person 的伴生对象并无别名,就不能如预期般有效调用 Person("John"),后者会隐式地触发伴生对象中的 apply 方法。

相关文章
相关标签/搜索