Scala是世界上最好的語言(一):Type Bound

52_hertz_whale發表於2019-03-04

這個語法應該是Scala最常用的語法之一。它比C#的型別約束走的更遠,因為它既可以表示上界,也可以表示下界。不過在此之前需要搞清幾個概念。

Variance Position

我們知道Java允許陣列協變,這會導致將一個子類新增到父類陣列時的異常:

String[] a1 = { "abc" };
Object[] a2 = a1;
a2[0] = new Integer(17);
String s = a1[0];
複製程式碼

從Scala一切皆方法的角度,就是陣列的update方法的引數是一個型別引數。更一般的來看,這裡update的引數是一個抗變點(contravariant position),或者說負向點(negative position)。其實我覺得負向點的說法更好,因為它可以與型變的概念區分開。一個抗變點不允許接受協變的型別引數。另外,還有2種型變點:協變點(covariant position),不變點(novariant position)。
由於型別引數本身也是一個型別,因此泛型是可以巢狀的。這給最終驗證型變註解帶來了一定的複雜性。在語言規範裡是這樣說明的:

The top-level of the type or template is always in covariant position. The variance position changes at the following constructs.

  • The variance position of a method parameter is the opposite of the variance position of the enclosing parameter clause.
  • The variance position of a type parameter is the opposite of the variance position of the enclosing type parameter clause.
  • The variance position of the lower bound of a type declaration or type parameter is the opposite of the variance position of the type declaration or parameter.

這裡型變發生了翻轉(flipped)。一個函式引數的型變點會翻轉,同時型別引數(如果有下界或者型變註解)的型變點也會翻轉。這是 Programming in Scala 中構造的示例:

abstract class Cat[-T, +U] {
def meow[W−](volume: T−, listener: Cat[U+, T−]−)
: Cat[Cat[U+, T−]−, U+]+ }
複製程式碼

注意到這裡Cat[U, T]的U和T互換了位置,因為他們所在的型變點發生了翻轉。比如,對於返回值而言,因為最外層的Cat的第一個型別引數是抗變的,所以U翻轉為協變點,T為抗變點(規則2)。同樣listener也發生了翻轉(規則1)。
原理上,方法的引數是逆變點,而返回值是協變點。這主要是基於里氏替換原則的推理。舉個例子:

abstract class Box[+F <: Fruit] {
  def fruit: F
  def contains(aFruit: Fruit) = fruit.name.equals(aFruit.name)
}
class AppleBox(apple: Apple) extends Box[Apple] {
  def fruit = apple
}
var fruitBox: Box[Fruit] = new AppleBox(new Apple)
var fruit: Fruit = fruitBox.fruit
複製程式碼

這裡的F是典型的協變應用。不過,假設這裡F是抗變的,那麼就會出現問題,因為作為父類的AppleBox需要返回一個apple,而子類fruitBox並不能替換它。
最後,Scala預設的行為是不變(novariant)。這也是一個更符合邏輯的抉擇。

Lower Bound

下界既可以是一個具體的類,也可以是一個型別引數。還是用 Programming in Scala 的例子,它通過下界允許對一個協變類呼叫包含逆變點的方法:

class Queue[+T] (private val leading: List[T], private val trailing: List[T] ) {
    ...
    def append[U >: T](x: U) = new Queue[U](leading, x :: trailing)
}
複製程式碼

這裡可以將一個Orange追加到Queue[Apple]上得到一個Queue[Fruit]。注意到當作為Queue[Fruit]時,U必須是Fruit的超類,因此這個下界防止了前面Java程式碼中的賦值錯誤。
更一般的來說,這裡的下界定義了一次翻轉(規則3)。這其實很好理解:我們總是在父類中呼叫方法append,而T是U的子類。
Programming in Scala 在這裡提到了術語宣告處型變(declaration-site variance)與使用處型變(use-site variance)。這裡的site是宣告型變的位置。Scala是宣告處型變的,這一點和C#相同。Java是使用處型變的,這種風格要求程式設計師必須很清楚類的可變性,缺少了編譯器的支援。

Reference

[1], Programming in Scala, 3rd, Martin Odersky, Lex Spoon, Bill Benners
[2], Scala Language Specification, scala-lang.org/files/archi…
[3], blog.codecentric.de/en/2015/04/…

相關文章