1. 程式人生 > >【Scala型別系統】型別引數化和變化型註解

【Scala型別系統】型別引數化和變化型註解

引言

型別引數化(Parameterized Types)可以用來編寫泛型類和特質,比如定義Set[T],這使得我們可以建立諸如Set[String]的型別。而變化型註解(Variance Annotation)定義了引數化型別的繼承關係,比如Set[String]是Set[AnyRef]的子型別。
這些語法可以讓我們實現資訊隱藏技術,同時它們也是編寫庫程式的基礎。

型別引數化

這裡以水果盒的程式碼作為例子:

abstract class Fruit {
  def name: String
}
class Orange extends Fruit {
  def name = "orange"
} class Apple extends Fruit { def name = "apple" } abstract class Box { def fruit: Fruit def contains(aFruit: Fruit) = fruit.name.equals(aFruit.name) } class OrangeBox(orange: Orange) extends Box { def fruit: Orange = orange } class AppleBox(apple: Apple) extends Box { def fruit: Apple = apple }

上面的例子,定義了Box的兩個子類,OrangeBox和AppleBox,這考慮的型別安全,因為fruit方法的返回值受到了Orange和Apple型別的特別限制。這也同樣帶來了維護程式碼量和型別安全的矛盾。
基於這個問題,Scala允許使用引數化型別,即你可以使用型別引數來代替實際的型別。型別引數可以認為是一個限制類型的別名。

根據型別繼承關係,Scala編譯器允許AppleBox或者OrangeBox的例項賦值給Box型別變數,在Box子類中fruit方法的實現返回了Apple或者Orange型別,這同樣可以賦值給Fruit型別的變數。

變化型註解

  • 假設聲明瞭class Orange extends Fruit
    ,而後class Box[A]中的A可以有字首+或-。
  • A,沒有任何註解,是不變型。該狀態下,Box[Orange]Box[Fruit]沒有任何繼承關係
  • +A,是協變型別。此時,Box[Orange]Box[Fruit]的子型別,並且變數宣告val f: Box[Fruit] = new Box[Orange]()是允許的。
  • -A,是逆變型別。此時,Box[Fruit]Box[Orange]的子型別,並且變數宣告val f: Box[Orange] = new Box[Fruit]()是允許的。


從總體來看,變化型別引數可以被認為是擴充套件泛型程式設計的型別檢查範圍的工具。它提供了額外的型別安全,即保證型別安全的前提下,為開發者提供了利用型別層級的可能性。

子型別多型

協變和逆變註解使得通過型別引數的繼承關係來推斷泛化型別的繼承關係,利用該註解可以限制類型引數在泛型類中一些可能的使用,Scala編譯器針對不恰當的使用進行檢查和報錯。

abstract class Box[+F <: Fruit] {
  def fruit: F
  def contains(aFruit: Fruit) = fruit.name.equals(aFruit.name)
}
class OrangeBox(orange: Orange) extends Box[Orange] {
  def fruit = orange
}
class AppleBox(apple: Apple) extends Box[Apple] {
  def fruit = apple
}

var fruitBox: Box[Fruit] = new AppleBox(new Apple)
var fruit: Fruit = fruitBox.fruit

上面的例子中,Box[+F]型別引數是協變的,那麼將Box[Apple]賦值給Box[Fruit]型別變數fruitBox是合法的。型別變數F用在函式返回型別中是合理的(比如Box.fruit),在fruitBox.fruit方法的呼叫中,保證返回給Fruit類的例項是一個更加具體的型別,由於型別引數是協變的,需要返回一個比Fruit型別更加具象的型別。

協變和逆變點

函式在引數上是逆變的,在返回值上則是協變的。通常而言,對於某個物件消費的值適用逆變,而對於它產出的值適用協變。
如果一個物件同時消費和產出某值,則型別應該保持不變。這通常適用於可變資料結構,比如標準庫中的Array、ArrayBuffer、ListBuffer等。

Scala編譯器檢查變化型註解的機制

為了核實變化型註解的正確性,Scala編譯器會把類或特質中可能用到型別引數的地方分類為正,負或中立。註解了+號的型別引數只能被用在正的位置上,而註解了-號的型別引數只能用在負的位置上。沒有變化型註解的型別引數可以用於任何位置。
Scala編譯器在檢查泛型類時,會跟蹤所有使用型別引數的位置,然後根據程式碼中變化型的位置來決定正確與否。如果型別引數出現在了禁止的位置,編譯器將會報錯。詳細的檢查規則可以參見這裡

為了對用到型別引數的地方進行分類,編譯器首先從型別引數的宣告開始,然後進入更深的內嵌層。
處於宣告類的最頂層被劃為正,預設情況下,更深的內嵌層的地方的分類會與它外層一致,但仍有幾種特殊情況可以使得型別引數的型別發生翻轉:

  1. 方法的值引數位置的引數型別
  2. 方法的型別引數子句位置的引數型別
  3. 型別引數的下界的下界引數型別
  4. 引數化型別的型別引數,當型別引數是逆變時
    型別的型別引數位置,比如C[Arg]的Arg,也有可能被翻轉,如果C的型別引數標註了+,那麼類別不變;如果C的型別引數標註了-,那麼當前類別被翻轉。

舉例解釋:
- def method(parameter: T)中,引數T在逆變的位置,遵循第一條規則
- def method[U <: T]中,引數T在逆變的位置,遵循第二條規則
- def method[U >: T]中,引數T在協變的位置,循序第二條和第三條規則
- class Box[-A]中,Box被宣告為逆變,此情況下,def method(parameter: Box[T])中的T應該是協變的位置,遵循第一條和第四條規則

下界

在一個方法的型別引數子句中的下界引數,為何從逆變位置翻轉成協變?
Box[+T]中的協變型別引數T,根據子型別多型性,只能變得更加具象,也就是說,受到T的限制,其實型別將沿著型別層級往下走。如果將T作為下界,使用U >: T中的U來作為型別約束,則是可行的。

class Box[+T](fruit: T) {
  def method[U >: T](p: U) = { new Box[U](p) }
}

var apple: Apple = new Apple
var box: Box[Fruit] = new Box[Orange](new Orange)
box = box.method(apple)

這個例子中,即使box在例項化的時候複製為一個子型別Box[Orange],Box.method的呼叫仍然是合法的。
通過語法U >: T,定義了T為U的下界。結果,U必須是T的超型別,method的引數也變為型別U而不是型別T,而方法返回值也變成了Box[U],取代了Box[T]。
通過把Apple物件加入到Box[Orange]中,返回結果為Box[Fruit]型別。

合法的變化型位置

下面的幾種變化型引數均為合法的語法格式:

  • abstract class Box[+A]{ def foo(): A }
  • abstract class Box[-A]{ def foo(a: A) }
  • abstract class Box[+A]{ def foo[B >: A](b: B) }
  • abstract class Box[-A]{ def foo[B <: A](): B}

參考資料