本文由 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
方法。