本文由 Yison 發表在 ScalaCool 團隊部落格。
上一篇
目錄
6. 一個單例物件的型別
Scala 的單例物件( object
) 是通過 class
實現的(顯然後者就像 JVM 的基礎構件)。然而你也會發現我們並不能像一個簡單的類一樣,輕鬆地獲得一個單例物件的型別……
我常常疑惑該如何傳一個單例物件給一個方法,對此我自己也非常驚訝。我的意思是指 obj: ExampleObj
是無效的,因為這種情況 ExampleObj
已經指向了例項,所以它有個 type
的成員,我們可以靠它解決問題。
下面的程式碼解釋了大概的方法:
object ExampleObjdef takeAnObject(obj: ExampleObj.type) = {
}takeAnObject(ExampleObj)複製程式碼
7. Scala 中的型變
術語 | 翻譯 |
---|---|
Variance | 型變 |
Invariant | 不變 |
Covariant | 協變 |
Contravariant | 逆變 |
Immutable | 不可變的 |
Mutable | 可變的 |
上述表格由譯者自主新增,避免造成誤解。
型變,通常可以解釋成型別之間依靠彼此的「相容性」,形成一種繼承的關係。最常見的例子就是當你要處理「容器」或「函式」的時候,有時就必須要處理型變(極其的常見!)。
Scala 跟 Java 一個重大的差異,就是它的「容器型別」預設是不變的!也就是說,如果你有一個定義為 Box[A]
的容器,然後在使用的時候將其中的型別引數 A
替換成 Fruit
,之後你就不能插入一個 Apple
型別(Fruit
子類)的值。
Scala 中的型變通過在「型別引數」前使用 +
和 -
符號來定義。
參見:www.slideshare.net/dgalichet/d…。
概念 | 描述 | 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 Fruitcase class Apple() extends Fruitcase class Orange() extends Fruitval l1: List[Apple] = Apple() :: Nilval 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 instanceval l3: List[AnyRef] = "" :: l2複製程式碼
值得一提的是,當存在不可變的 collection 時,協變是安全的。如果 collection 可變,則不成立。這裡典型的例子是 Array[T]
,它是不變的。下面就來看看「不變」對我們來說意味著什麼,以及它是如何讓我們免於錯誤:
// won't compileval 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 Awesomeclass BC extends Base with Cool// as you might expect, you can upcast these instances into any of the traits they've mixed-inval ba: BA = new BAval bc: Base with Cool = new BCval b1: Base = baval b2: Base = bcba.abc.cb1.b複製程式碼
目前而言,你應該都比較好理解。現在讓我們來討論下「鑽石問題」,熟悉 C++ 的讀者可能一直在期待吧。鑽石問題(菱形繼承問題)主要描述的是在「多重繼承」的情況下,我們「無法明確想要繼承什麼」的處境。如果你認為特質也類似多重繼承一樣,下圖揭示了這個問題。
7.2 型別線性化 VS 鑽石問題
要說明「鑽石問題」,我們只要有一個 B
、C
中的覆蓋實現就行了。當我們呼叫 D
中的 common
方法的時候,產生了歧義 — 我們到底是繼承了 B
還是 C
的方法?在 Scala 裡,如果僅僅只有一個覆蓋方法的情況下,這個問題很簡單 — 就是這個覆蓋方法。但假使是更復雜的情況呢?讓我們來研究一下:
- class
A
定義了方法common
,返回a
; - trait
B
覆蓋common
,返回b
; - trait
C
覆蓋common
,返回c
; - class
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 Cclass 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
。
8. Refined Types (refinements)
Refinements 可以很簡單地理解為「匿名的子類化」。所以在原始碼中,可以是類似這個樣子:
class Entitytrait 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
),這本身並沒有真的擴充了型別系統。但包物件們提供了一種相當有用的模式,可以一起引入一堆東西,此外編譯器也會在它們那尋找隱式的值。
宣告一個包物件很簡單,只要一起使用 package
和 object
關鍵字就行了,就像這樣子:
// src/main/scala/com/garden/apples/package.scalapackage com.gardenpackage 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 = Stringtype Age = Intval data: Map[User, Age] = Map.empty複製程式碼
通過這樣的技巧,Map 的定義一下子變得很清晰。如果我們僅僅只使用一個 Sting =>
的 map,程式碼的可讀性就不那麼好了。雖然我們仍舊可以堅持使用我們的原始型別(也許是出於如效能方面的考慮),但使用別名能讓這個類後續的讀者更容易理解。
Int
注意,當你要為一個類建立別名的時候,並不會為它的伴生物件也建立別名。舉個例子,假使你定義了
case class Person(name: String)
以及一個別名type User = Person
,呼叫User("
就會出錯。因為
John"
)Person
的伴生物件並沒有別名,就不能如預期般有效呼叫Person("
,後者會隱式地觸發伴生物件中的
John"
)apply
方法。