本文由 Yison 发表在 ScalaCool 团队博客。算法
Scala 的单例对象( object
) 是经过 class
实现的(显而后者就像 JVM 的基础构件)。然而你也会发现咱们并不能像一个简单的类同样,轻松地得到一个单例对象的类型……app
我经常疑惑该如何传一个单例对象给一个方法,对此我本身也很是惊讶。个人意思是指 obj: ExampleObj
是无效的,由于这种状况 ExampleObj
已经指向了实例,因此它有个 type
的成员,咱们能够靠它解决问题。ide
下面的代码解释了大概的方法:函数
object ExampleObj
def takeAnObject(obj: ExampleObj.type) = {}
takeAnObject(ExampleObj)复制代码
术语 | 翻译 |
---|---|
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]
。
首先,让咱们看看关于「特质」最简单的一个问题:咱们如何将多个特质混入到一个类型中,就像若是你来自 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++ 的读者可能一直在期待吧。钻石问题(菱形继承问题)主要描述的是在「多重继承」的状况下,咱们「没法明确想要继承什么」的处境。若是你认为特质也相似多重继承同样,下图揭示了这个问题。
要说明「钻石问题」,咱们只要有一个 B
、C
中的覆盖实现就好了。当咱们调用 D
中的 common
方法的时候,产生了歧义 — 咱们究竟是继承了 B
仍是 C
的方法?在 Scala 里,若是仅仅只有一个覆盖方法的状况下,这个问题很简单 — 就是这个覆盖方法。但假使是更复杂的状况呢?让咱们来研究一下:
A
定义了方法 common
,返回 a
;B
覆盖 common
,返回 b
;C
覆盖 common
,返回 c
;D
同时继承 B
和 C
;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
上尝试这种方法 — 若是你运行代码,它应该会前后对 C
和 B
进行线性化,从而产生一个为 "b"
的结果。而且,你也能够简单地利用「最右取胜」的原则来简化线性化规则的理解,但尽管这个有用,却并无展示整个算法的全貌。
值得一提的是,咱们也能够经过这种技术来获知「谁是咱们的超类?」。如同在线性化类型中「朝左看」同样简单,你就能知道任何类的超类是谁。因此在咱们的 D1
例子中,C
的超类是 B
。
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) = ()
}复制代码
Scala 在 2.8 版本中引入了包对象(Package Object
),这自己并无真的拓展了类型系统。但包对象们提供了一种至关有用的模式,能够一块儿引入一堆东西,此外编译器也会在它们那寻找隐式的值。
声明一个包对象很简单,只要一块儿使用 package
和 object
关键字就好了,就像这样子:
// 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复制代码
类型别名(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
方法。