變異讓您可以控制類型參數在子類型化方面的行為。Scala 支援 泛型類別 的類型參數變異註解,讓它們可以是協變、逆變或不變,如果沒有使用註解。在類型系統中使用變異,讓我們可以在複雜類型之間建立直觀的關聯。
class Foo[+A] // A covariant class
class Bar[-A] // A contravariant class
class Baz[A] // An invariant class
不變
預設情況下,Scala 中的類型參數是不變的:類型參數之間的子類型化關係不會反映在參數化類型中。為了探討為什麼會這樣運作,我們來看一個簡單的參數化類型,可變方塊。
class Box[A](var content: A)
我們將在其中放入 Animal
類型的值。此類型定義如下
abstract class Animal {
def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
abstract class Animal:
def name: String
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal
我們可以說 Cat
是 Animal
的子類型,而 Dog
也是 Animal
的子類型。這表示下列程式碼是類型良好的
val myAnimal: Animal = Cat("Felix")
方塊呢?Box[Cat]
是 Box[Animal]
的子類型嗎,就像 Cat
是 Animal
的子類型?乍看之下,這似乎可行,但如果我們嘗試這麼做,編譯器會告訴我們發生錯誤
val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix"))
val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile
val myAnimal: Animal = myAnimalBox.content
val myCatBox: Box[Cat] = Box[Cat](Cat("Felix"))
val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile
val myAnimal: Animal = myAnimalBox.content
為什麼這會是個問題?我們可以從方塊中取得貓,而它仍然是動物,不是嗎?沒錯。但這不是我們能做的全部。我們也可以用不同的動物取代方塊中的貓
myAnimalBox.content = Dog("Fido")
動物方塊中現在有一隻狗。這很好,你可以將狗放入動物方塊中,因為狗是動物。但我們的動物方塊是一個貓方塊!你不能將狗放入貓方塊中。如果我們可以,然後嘗試從貓方塊中取出貓,結果會是一隻狗,這會破壞類型健全性。
val myCat: Cat = myCatBox.content //myCat would be Fido the dog!
從這裡,我們必須得出結論,即使 Cat
和 Animal
具有子類型關係,Box[Cat]
和 Box[Animal]
也不能具有子類型關係。
協變
我們上面遇到的問題是,因為我們可以將狗放入動物方塊中,所以貓方塊不能成為動物方塊。
但如果我們不能將狗放入方塊中會怎樣?那麼我們可以將貓取回,這不是問題,所以它可以遵循子類型關係。事實證明,這確實是我們可以做的事情。
class ImmutableBox[+A](val content: A)
val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix"))
val animalBox: ImmutableBox[Animal] = catbox // now this compiles
class ImmutableBox[+A](val content: A)
val catbox: ImmutableBox[Cat] = ImmutableBox[Cat](Cat("Felix"))
val animalBox: ImmutableBox[Animal] = catbox // now this compiles
我們說 ImmutableBox
在 A
中是協變的,這由 A
前面的 +
表示。
更正式地說,這給了我們以下關係:給定一些 class Cov[+T]
,如果 A
是 B
的子類型,那麼 Cov[A]
是 Cov[B]
的子類型。這允許我們使用泛型建立非常有用且直觀的子類型關係。
在以下不太做作的示例中,方法 printAnimalNames
將接受一個動物列表作為參數,並將它們的名稱逐行打印出來。如果 List[A]
不是協變的,則最後兩個方法調用將無法編譯,這將嚴重限制 printAnimalNames
方法的用處。
def printAnimalNames(animals: List[Animal]): Unit =
animals.foreach {
animal => println(animal.name)
}
val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom"))
val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))
// prints: Whiskers, Tom
printAnimalNames(cats)
// prints: Fido, Rex
printAnimalNames(dogs)
逆變
我們已經看到,我們可以通過確保我們不能將某物放入協變類型中,而只能取出某物來實現協變。如果我們有相反的情況,即你可以將某物放入其中,但不能取出會怎樣?如果我們有序列化器之類的東西,它會獲取類型為 A 的值並將它們轉換為序列化格式,就會出現這種情況。
abstract class Serializer[-A] {
def serialize(a: A): String
}
val animalSerializer: Serializer[Animal] = new Serializer[Animal] {
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
}
val catSerializer: Serializer[Cat] = animalSerializer
catSerializer.serialize(Cat("Felix"))
abstract class Serializer[-A]:
def serialize(a: A): String
val animalSerializer: Serializer[Animal] = new Serializer[Animal]():
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
val catSerializer: Serializer[Cat] = animalSerializer
catSerializer.serialize(Cat("Felix"))
我們說 Serializer
在 A
中是逆變的,這由 A
前面的 -
表示。更通用的序列化器是更具體序列化器的子類型。
更正式地說,這給了我們相反的關係:給定一些 class Contra[-T]
,如果 A
是 B
的子類型,那麼 Contra[B]
是 Contra[A]
的子類型。
不變性和變異性
不變性構成使用變異背後設計決策的重要部分。例如,Scala 的集合系統性地區分 可變和不可變集合。主要問題在於協變可變集合會破壞類型安全性。這就是 List
是協變集合,而 scala.collection.mutable.ListBuffer
是不變集合的原因。 List
是封裝 scala.collection.immutable
中的集合,因此保證對所有人都是不可變的。而 ListBuffer
是可變的,也就是說,你可以變更、新增或移除 ListBuffer
的元素。
為了說明協變和可變性的問題,假設 ListBuffer
是協變的,那麼以下有問題的範例會編譯(實際上它無法編譯)
import scala.collection.mutable.ListBuffer
val bufInt: ListBuffer[Int] = ListBuffer[Int](1,2,3)
val bufAny: ListBuffer[Any] = bufInt
bufAny(0) = "Hello"
val firstElem: Int = bufInt(0)
如果上述程式碼可行,那麼評估 firstElem
會因 ClassCastException
而失敗,因為 bufInt(0)
現在包含 String
,而不是 Int
。
ListBuffer
的不變性表示 ListBuffer[Int]
不是 ListBuffer[Any]
的子類型,儘管 Int
是 Any
的子類型,因此 bufInt
無法指定為 bufAny
的值。
與其他語言的比較
某些與 Scala 相似的語言以不同的方式支援變異。例如,Scala 中的變異註解與 C# 中的註解非常相似,其中在定義類別抽象時新增註解(宣告位置變異)。然而,在 Java 中,變異註解是在使用類別抽象時由客戶端提供的(使用位置變異)。
Scala 傾向於不可變類型,這使得協變和反變類型比其他語言更常見,因為可變泛型類型必須是不變的。