【Scala之旅】型別引數

weixin_33807284發表於2018-04-07

本節翻譯自

綜述:在Scala中,你可以用型別引數來實現類和函式,這樣的類和函式可以用於多種型別;你可以指定型別如何根據其型別引數的變化而變化。

泛型類

泛型類將類的型別作為引數。他們作為集合類的時候尤其有用。

定義一個泛型類

泛型類將型別作為引數放進中括號 [] 中。有一個慣例是將大寫字母 A 作為型別引數的標誌符,儘管任何引數型別名字都可以被使用。

class Stack[A] {
  private var elements: List[A] = Nil
  def push(x: A) { elements = x :: elements }
  def peek: A = elements.head
  def pop(): A = {
    val currentTop = peek
    elements = elements.tail
    currentTop
  }
}

這個 Stack 的實現類可以將任何型別 A 作為引數。這意味著基礎列表 var elements: List[A] = Nil 只能儲存型別為 A 的元素。而方法 def push 只接受型別為 A 的物件(注意:elements = x :: elements 通過將 x 預先新增到當前的 elements 建立新的列表,再重新分配給 elements)。

使用

通過將型別放入中括號中代替 A 來使用泛型類:

val stack = new Stack[Int]
stack.push(1)
stack.push(2)
println(stack.pop)  // prints 2
println(stack.pop)  // prints 1

這個例項 stack 只能取整數。然而,如果型別引數有子型別,這些可以通過:

class Fruit
class Apple extends Fruit
class Banana extends Fruit

val stack = new Stack[Fruit]
val apple = new Apple
val banana = new Banana

stack.push(apple)
stack.push(banana)

AppleBanana 都擴充套件自 Fruit,所以我們可以把 AppleBanana 壓入 Fruit 的堆疊中。

注意:泛型型別的子型別是不變的。這意味著如果我們有一堆型別為 Stack[Char] 的字元堆疊,那麼它不能用作 Stack[Int] 型別的整數堆疊。這是有缺陷的,因為我們能夠將數字真正輸入到字元堆疊。總之,當且僅當 B = A 時,Stack[A] 才是 Stack[B] 的一個子型別。由於這可能是相當嚴格的,Scala提供了一個型別引數註解機制來控制泛型型別的子型別行為。

型變

型變是複雜型別的子型別關係以及它們的元件型別的子型別關係之間的相關性。Scala支援在泛型類的型別引數中使用型變註解;如果沒有使用註解,則允許它們是協變的、逆變的或不變的。在型別系統中使用型變允許我們在複雜型別之間建立直觀的聯絡,而缺乏形變可以限制類抽象的重用。

class Foo[+A] // A covariant class
class Bar[-A] // A contravariant class
class Baz[A]  // An invariant class

協變

一個泛型類的型別引數 A 可以通過使用註解 +A 進行協變。對一些 class List[A],將 A 協變意味著若有 AB 兩種型別且 AB 的子類,則 List[A]List[B] 的一個子類。這使我們能夠使用泛型做出非常有用和直觀的子型別關係。

個人注:這裡的意思是,原先雖然 AB 的子類,但 List[A]List[B] 並沒有任何關係。而將原來的泛型類加上型變註解(class List[+A])之後,List[A] 就變成了 List[B] 的一個子類。這意味著我們使用泛型做出一個新的的子型別關係。

考慮一下這個簡單的類結構:

abstract class Animal {
  def name: String
}
case class Cat(name: String) extends Animal
case class Dog(name: String) extends Animal

CatDog 都是 Animal 的子類。Scala 標準庫有一個通用的不可變的 sealed abstract class List[+A] 類,它的型別引數 A 是協變的。這意味著 List[Cat] 是一個 List[Animal],而 List[Dog] 也是一個 List[Animal]。直觀地說,Cat 的列表和 Dog 的列表都是一個 Animal 列表,你應該能夠用它代替一個 List[Animal]

在下面的例子中,方法 printAnimalNames 將接收一個 Animal 列表作為引數,然後在每個新行中列印它們自己的名字。如果 List[A] 不是協變的,最後兩個方法呼叫將無法編譯通過,這將嚴重限制 printAnimalNames 方法的有效性。

object CovarianceTest extends App {
  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"))

  printAnimalNames(cats)
  // Whiskers
  // Tom

  printAnimalNames(dogs)
  // Fido
  // Rex
}

逆變

一個泛型類的型別引數 A 可以通過使用註解 -A 進行逆變。這與使用類和它的型別引數建立子型別關係類似,但與我們用協變得到的型別相反。也就是說,對一些 class Writer[A]A 逆變意味著:若有兩種型別 ABAB 的子類,則 Writer[B]Writer[A] 的子類。

考慮 CatDogAnimal 類上面,定義下面的例子:

abstract class Printer[-A] {
  def print(value: A): Unit
}

Printer[A] 是一個簡單的類,它知道如何列印出一些型別 A。讓我們定義一些特定型別的子類:

class AnimalPrinter extends Printer[Animal] {
  def print(animal: Animal): Unit =
    println("The animal's name is: " + animal.name)
}

class CatPrinter extends Printer[Cat] {
  def print(cat: Cat): Unit =
    println("The cat's name is: " + cat.name)
}

如果 Printer[Cat] 知道如何列印任何 Cat 到控制檯,Printer[Animal] 知道如何列印任何 Animal 到控制檯,Printer[Animal] 應該也會知道如何列印任何 Cat,這是講得通的。但與此相反並不適用,因為 Printer[Cat] 不知道如何列印任何 Animal 到控制檯。因此,我們應該能夠用 Printer[Animal] 代替 Printer[Cat],如果我們希望,使 Printer[A] 逆變允許我們這樣做。

object ContravarianceTest extends App {
  val myCat: Cat = Cat("Boots")

  def printMyCat(printer: Printer[Cat]): Unit = {
    printer.print(myCat)
  }

  val catPrinter: Printer[Cat] = new CatPrinter
  val animalPrinter: Printer[Animal] = new AnimalPrinter

  printMyCat(catPrinter)
  printMyCat(animalPrinter)
}

這個程式的輸出將會是:

The cat's name is: Boots
The animal's name is: Boots

不變

泛型類在Scala中預設是不變的。這意味著,它們既不是協變和逆變。在接下來的例子中,Container 類是不變的。一個 Container[Cat] 不是一個 Container[Animal],反之亦然。

class Container[A](value: A) {
  private var _value: A = value
  def getValue: A = _value
  def setValue(value: A): Unit = {
    _value = value
  }
}

似乎一個 Container[Cat] 自然也應該是一個 Container[Animal],但允許一個可變的泛型類協變會不安全。在這個例子中,Container 是不變的非常重要。假設 Container 實際上是協變,可能發生以下情況:

val catContainer: Container[Cat] = new Container(Cat("Felix"))
val animalContainer: Container[Animal] = catContainer
animalContainer.setValue(Dog("Spot"))
val cat: Cat = catContainer.getValue // Oops, we'd end up with a Dog assigned to a Cat

幸運的是,編譯器在很久之前就阻止了我們。

其他例子

另一個可以幫助理解型變的例子是Scala標準庫的 trait Function1[-T, +R]Function1 表示一個函式有一個引數,第一個型別引數 T 表示引數型別,第二個型別引數 R 代表返回型別。 Function1 對引數型別逆變,對其返回型別協變。 在這個例子中,我們將使用文字元號 A => B 代表 Function1(A,B)

假設類似 CatDogAnimal 繼承樹使用前,加上以下幾點:

class SmallAnimal
class Mouse extends SmallAnimal

假設我們的函式接受 Animal 型別,並返回它們吃的食物種類。如果我們像 Cat => SmallAnimal(因為貓吃小動物),但用 Animal => Mouse 來代替,我們的程式依然可以繼續工作。直觀來講,Animal => Mouse 仍將接受 Cat 作為引數,因為 CatAnimal,並且返回 Mouse,它也是 SmallAnimal。既然我們可以安全地、無形地將前者替換成後者,我們可以說 Animal => MouseCat => SmallAnimal 的子型別。

與其他語言相比

一些類似於Scala的語言以不同的方式支援型變。在Scala中型變的註解相似於c#,它在定義類抽象時新增註解(宣告位置變數)。但是,在Java中,當使用類抽象時(客戶端使用型變),客戶端會給出型變註解。

型別上界

在 Scala 中,型別引數抽象型別可能受到型別邊界的約束。這種型別邊界限制了型別變數的具體值,並且可能揭示了關於這些型別成員的更多資訊。型別上邊界 T <: A 宣告型別變數 T 是型別 A 的子型別。下面是一個示例,它演示了類 PetContainer 的型別引數的型別上界繫結:

abstract class Animal {
 def name: String
}

abstract class Pet extends Animal {}

class Cat extends Pet {
  override def name: String = "Cat"
}

class Dog extends Pet {
  override def name: String = "Dog"
}

class Lion extends Animal {
  override def name: String = "Lion"
}

class PetContainer[P <: Pet](p: P) {
  def pet: P = p
}

val dogContainer = new PetContainer[Dog](new Dog)
val catContainer = new PetContainer[Cat](new Cat)
//  val lionContainer = new PetContainer[Lion](new Lion)
//                         ^this would not compile

class PetContainer 取一個型別引數 P,它必須是 Pet 的子型別。DogCat 都是 Pet 的子類,所以我們能建立一個新的 PetContainer[Dog] 和一個新的 PetContainer[Cat]。然而。如果我們嘗試去建立一個 PetContainer[Lion],我們將得到一下錯誤:

type arguments [Lion] do not conform to class PetContainer's type parameter bounds [P <: Pet]

這是因為 Lion 不是 Pet 的子類。

型別下界

型別上界限制了一個型別為另一種型別的子型別,而型別下界則宣告一個型別為另一種型別的超型別。B >: A 語法表達了型別引數 B 或者抽象型別 B 是型別 A 的超型別。在大多數情況下,A 將是類的型別引數,B 將是方法的型別引數。

這裡是一個非常有用的例子:

trait Node[+B] {
  def prepend(elem: B): Unit
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
  def prepend(elem: B) = ListNode[B](elem, this)
  def head: B = h
  def tail = t
}

case class Nil[+B]() extends Node[B] {
  def prepend(elem: B) = ListNode[B](elem, this)
}

這個程式實現了一個單連結串列。Nil 代表一個空元素(即一個空列表)。class ListNode 是一個節點,其中包含了 B 型別的元素(head)和對列表其餘部分的引用(tail)。class Node 和他的子類是協變的,因為我們有 +B

然而這個程式並不能編譯,因為 prepend 的引數 elem 的型別是 B,我們將其宣告為協變。這是行不通的,因為一個函式在引數型別中應該是逆變的,在結果型別中是協變的。

為了解決這個問題,我們需要翻轉 prepend 引數型別 elem 的協變。我們通過引入一個新的型別引數 U 來實現這一點,它有 B 作為一個型別下界限制。

trait Node[+B] {
  def prepend[U >: B](elem: U)
}

case class ListNode[+B](h: B, t: Node[B]) extends Node[B] {
  def prepend[U >: B](elem: U) = ListNode[U](elem, this)
  def head: B = h
  def tail = t
}

case class Nil[+B]() extends Node[B] {
  def prepend[U >: B](elem: U) = ListNode[U](elem, this)
}

現在我們可以做到以下幾點:

trait Bird
case class AfricanSwallow() extends Bird
case class EuropeanSwallow() extends Bird

val africanSwallowList= ListNode[AfricanSwallow](AfricanSwallow(), Nil())
val birdList: Node[Bird] = africanSwallowList
birdList.prepend(new EuropeanSwallow)

Node[Bird] 可以被分配到 africanSwallowList 上,但隨後接受 EuropeanSwallow

多型方法

Scala中的方法可以通過型別和值進行引數化。語法類似於泛型類。型別引數用方括號括起來,而值引數用圓括號括起來。

下面是一個例子:

def listOfDuplicates[A](x: A, length: Int): List[A] = {
    if (length < 1)
        Nil
    else
        x :: listOfDuplicates(x, length - 1)
}
println(listOfDuplicates[Int](3, 4))  // List(3, 3, 3, 3)
println(listOfDuplicates("La", 8))  // List(La, La, La, La, La, La, La, La)

listOfDuplicayes 方法獲取一個型別引數 A 還有值引數 xlength。值 x 的型別是 A。如果 length < 1 我們返回一個空列表。否則,我們將 x 加入到遞迴呼叫 listOfDuplicates 返回的重複列表中。(注意 :: 表示左側的元素為右側的序列)。

在第一個示例呼叫中,我們通過寫入 [Int] 來顯式提供型別引數。因此,第一個引數必須是一個 Int,返回型別將是 List[Int]

第二個示例呼叫顯示你並不總是需要顯式提供型別引數。 編譯器通常可以根據上下文或值引數的型別來推斷它。 在這個例子中,"La"是一個 String,因此編譯器知道 A 必須是 String

相關文章