[譯] 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 方法。

相關文章
相關標籤/搜索