Scala 導覽

變異

語言

變異讓您可以控制類型參數在子類型化方面的行為。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

我們可以說 CatAnimal 的子類型,而 Dog 也是 Animal 的子類型。這表示下列程式碼是類型良好的

val myAnimal: Animal = Cat("Felix")

方塊呢?Box[Cat]Box[Animal] 的子類型嗎,就像 CatAnimal 的子類型?乍看之下,這似乎可行,但如果我們嘗試這麼做,編譯器會告訴我們發生錯誤

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!

從這裡,我們必須得出結論,即使 CatAnimal 具有子類型關係,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

我們說 ImmutableBoxA 中是協變的,這由 A 前面的 + 表示。

更正式地說,這給了我們以下關係:給定一些 class Cov[+T],如果 AB 的子類型,那麼 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"))

我們說 SerializerA 中是逆變的,這由 A 前面的 - 表示。更通用的序列化器是更具體序列化器的子類型。

更正式地說,這給了我們相反的關係:給定一些 class Contra[-T],如果 AB 的子類型,那麼 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] 的子類型,儘管 IntAny 的子類型,因此 bufInt 無法指定為 bufAny 的值。

與其他語言的比較

某些與 Scala 相似的語言以不同的方式支援變異。例如,Scala 中的變異註解與 C# 中的註解非常相似,其中在定義類別抽象時新增註解(宣告位置變異)。然而,在 Java 中,變異註解是在使用類別抽象時由客戶端提供的(使用位置變異)。

Scala 傾向於不可變類型,這使得協變和反變類型比其他語言更常見,因為可變泛型類型必須是不變的。

此頁面的貢獻者