類型參數變異控制參數化類型的子類型化(例如類別或特質)。
為了說明變異,讓我們假設以下類型定義
trait Item { def productNumber: String }
trait Buyable extends Item { def price: Int }
trait Book extends Buyable { def isbn: String }
讓我們也假設以下參數化類型
// an example of an invariant type
trait Pipeline[T] {
def process(t: T): T
}
// an example of a covariant type
trait Producer[+T] {
def make: T
}
// an example of a contravariant type
trait Consumer[-T] {
def take(t: T): Unit
}
// an example of an invariant type
trait Pipeline[T]:
def process(t: T): T
// an example of a covariant type
trait Producer[+T]:
def make: T
// an example of a contravariant type
trait Consumer[-T]:
def take(t: T): Unit
一般而言,有下列三種變異模式
- 不變—預設值,寫成
Pipeline[T]
- 協變—加上
+
註解,例如Producer[+T]
- 逆變—加上
-
註解,例如Consumer[-T]
我們現在將詳細說明此註解的意義以及我們使用它的原因。
不變類型
預設情況下,像 Pipeline
之類的類型在其類型參數(本例中為 T
)中是不變的。這表示像 Pipeline[Item]
、Pipeline[Buyable]
和 Pipeline[Book]
之類的類型彼此之間沒有子類型關係。
這是理所當然的!假設下列方法使用兩個 Pipeline[Buyable]
類型的值,並根據價格將其參數 b
傳遞給其中一個
def oneOf(
p1: Pipeline[Buyable],
p2: Pipeline[Buyable],
b: Buyable
): Buyable = {
val b1 = p1.process(b)
val b2 = p2.process(b)
if (b1.price < b2.price) b1 else b2
}
def oneOf(
p1: Pipeline[Buyable],
p2: Pipeline[Buyable],
b: Buyable
): Buyable =
val b1 = p1.process(b)
val b2 = p2.process(b)
if b1.price < b2.price then b1 else b2
現在,回想一下我們的類型之間有下列子類型關係
Book <: Buyable <: Item
我們無法將 Pipeline[Book]
傳遞給方法 oneOf
,因為在其實作中,我們使用 Buyable
類型的值呼叫 p1
和 p2
。Pipeline[Book]
預期 Book
,這可能會導致執行時期錯誤。
我們無法傳遞 Pipeline[Item]
,因為對其呼叫 process
只保證會傳回 Item
;然而,我們應該傳回 Buyable
。
為什麼是不變的?
事實上,類型 Pipeline
需要是不變的,因為它將其類型參數 T
同時用作參數和傳回類型。基於相同原因,Scala 函式庫中的一些類型(例如 Array
或 Set
)也是不變的。
協變類型
與不變的 Pipeline
相反,類型 Producer
標記為協變,方法是在類型參數前加上 +
。這是有效的,因為類型參數僅用於回傳位置。
將其標記為協變表示我們可以在預期 Producer[Buyable]
的地方傳遞 (或回傳) Producer[Book]
。事實上,這是合理的。Producer[Buyable].make
的類型僅承諾回傳 Buyable
。作為 make
的呼叫者,我們也很樂意接受 Book
,它是 Buyable
的子類型,也就是說,它至少是 Buyable
。
以下範例說明了這一點,其中函數 makeTwo
預期 Producer[Buyable]
def makeTwo(p: Producer[Buyable]): Int =
p.make.price + p.make.price
傳遞書籍的生產者完全沒問題
val bookProducer: Producer[Book] = ???
makeTwo(bookProducer)
在 makeTwo
中呼叫 price
對書籍來說仍然有效。
不可變容器的協變類型
在處理不可變容器時,您會遇到許多協變類型,例如可以在標準函式庫中找到的類型 (例如 List
、Seq
、Vector
等)。
例如,List
和 Vector
大約定義為
class List[+A] ...
class Vector[+A] ...
這樣,您可以在預期 List[Buyable]
的地方使用 List[Book]
。這在直覺上也說得通:如果您預期的是可以購買的物品集合,那麼給您一個書籍集合應該是沒問題的。在我們的範例中,它們有一個額外的 ISBN 方法,但您可以忽略這些額外功能。
反變類型
與標記為協變的類型 Producer
相反,類型 Consumer
會在類型參數前加上 -
,標記為反變。這是有效的,因為類型參數僅用於引數位置。
將其標記為反變表示我們可以在預期 Consumer[Buyable]
的地方傳遞 (或傳回) Consumer[Item]
。也就是說,我們有子類型關係 Consumer[Item] <: Consumer[Buyable]
。請記住,對於類型 Producer
,情況正好相反,我們有 Producer[Buyable] <: Producer[Item]
。
事實上,這是合理的。方法 Consumer[Item].take
接受 Item
。作為 take
的呼叫方,我們也可以提供 Buyable
,而 Consumer[Item]
會很樂意接受,因為 Buyable
是 Item
的子類型,也就是說,它至少是一個 Item
。
消費者的反變類型
反變類型比協變類型少見得多。就像在我們的範例中,你可以將它們視為「消費者」。你可能會遇到的標記為反變的最重要的類型是函數
trait Function[-A, +B] {
def apply(a: A): B
}
trait Function[-A, +B]:
def apply(a: A): B
它的參數類型 A
標記為反變 A
—它會消耗類型 A
的值。相反地,它的結果類型 B
標記為協變—它會產生類型 B
的值。
以下是說明函數上的變異註解所引發的子類型關係的一些範例
val f: Function[Buyable, Buyable] = b => b
// OK to return a Buyable where a Item is expected
val g: Function[Buyable, Item] = f
// OK to provide a Book where a Buyable is expected
val h: Function[Book, Buyable] = f
摘要
在本節中,我們遇到了三種不同的變異
- 生產者通常是協變的,並使用
+
標記其類型參數。這也適用於不可變集合。 - 消費者通常是反變的,並使用
-
標記其類型參數。 - 既是生產者又是消費者的類型必須是不變的,並且不需要在其類型參數上做任何標記。像
Array
這樣的可變集合屬於此類別。