1. 程式人生 > >scala協變逆變上界下界---理解篇

scala協變逆變上界下界---理解篇

Scala的協變和逆變上界與下界

1. 引子:

為了弄懂scala中協變和逆變這兩個概念,查閱了不少資料,但是還是要自己總結一下,會記得比較深刻。

那就從java和scala的對比說起吧。

java中:

如果你很理解java的泛型,就會知道:比如給定一個類B,和他的父類A。

那麼用多型, A a = new B 編譯器是允許的。

但是如果泛型B的集合直接賦給父類A的集合。List<A> aList = new ArrayList<B>(); 

舉個簡單的例子:

Object s = "abc";
List<Object> objects = new ArrayList<String>();
編譯不通過,編譯器提示:
Type mismatch: cannot convert from ArrayList<String> to List<Object>

scala中:

我們知道sting是AnyRef的子類。直接賦值是可以的。 如果是將string的集合賦值給AnyRef的集合,在scala中也是可以的。
scala> val s:AnyRef = "abc"
s: AnyRef = abc

scala> var objects : List[AnyRef] = List[String]("abc","123")
objects: List[AnyRef] = List(abc, 123)
原因就在於List其實是支援協變的。

2. 協變

[+T], covariant (or “flexible”) in its type parameter T,類似Java中的(? extends T), 即可以用T和T的子類來替換T,里氏替換原則。

可以看到List的定義:

 type List[+A] = scala.collection.immutable.List[A]

協變的符號是[+A],意味著支援泛型A的子類集合向A進行賦值。

在這個例子裡是List的是支援協變的。

3.不變

[T], invariant  in its type parameter T

在scala可變集合中,MutableList是一個不變的型別。

定義:

class MutableList[A]
extends AbstractSeq[A]
......
我們還用相似的場景,將一個MutableList的string型別付給MutableList的AnyRef型別,這樣的賦值是不允許的。

編譯器會提示class MutableList is invariant in type A. 即在型別A中MutableList是不支援協變和逆變的。

scala> import scala.collection.mutable._
import scala.collection.mutable._

scala> val a : MutableList[AnyRef] = MutableList[String]("abc")
<console>:10: error: type mismatch;
 found   : scala.collection.mutable.MutableList[String]
 required: scala.collection.mutable.MutableList[AnyRef]
Note: String <: AnyRef, but class MutableList is invariant in type A.
You may wish to investigate a wildcard type such as `_ <: AnyRef`. (SLS 3.2.10)
       val a : MutableList[AnyRef] = MutableList[String]("abc")
                                                        ^

4. 逆變

[-T], contravariant, 類似(? supers T) 
if T is a subtype of type S, this would imply that Queue[S] is a subtype of Queue[T]

只能用T的父類來替換T。是逆里氏替換原則。

在  scala.actors.OutputChannel 這個trait是一個逆變的型別。

trait OutputChannel[-Msg] {
......

對於OutputChannel[String], 支援的操作就是輸出一個string, 同樣OutputChannel[AnyRef]也一定可以支援輸出一個string, 因為它支援輸出任意一個AnyRef(它要求的比OutputChannel[String]少) 。

但反過來就不行, OutputChannel[String]只能輸出String, 顯然不能替換OutputChannel[AnyRef]

5.上界與下屆

Scala的上界和下界比較難理解, 因為和Java裡面的界不是一個意思...

Java中, (? extends T), T稱為上界, 比較容易理解, 代表T和T的子類, (? supers T), T稱為下界

Scala中, 界卻用於泛型類中的方法的引數型別上, 如下面的例子, 
對於Queue中的append的型別引數直接寫T, 會報錯 (error: covariant type T occurs in contravariant position in type T of value x) 
這個地方比較複雜, 簡單的說就是Scala內部實現是, 把類中的每個可以放型別的地方都做了分類(+, –, 中立), 具體分類規則不說了 
對於這裡最外層類[+T]是協變, 但是到了方法的型別引數時, 該位置發生了翻轉, 成為-逆變的位置, 所以你把T給他, 就會報錯說你把一個協變型別放到了一個逆變的位置上

所以這裡的處理的方法就是, 他要逆變, 就給他個逆變, 使用[U >: T], 其中T為下界, 表示T或T的超類, 這樣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) //使用T的超類U來替換T
}

同樣對於上界也是,

trait OutputChannel[-T]
{
    def write [U<:T] (x: U) //使用T的子類U來替換T
}

總結:

1. 協變

[+T], covariant (or “flexible”) in its type parameter T,類似Java中的(? extends T), 即可以用T和T的子類來替換T,里氏替換原則。

2. 不變

不支援T的子類或者父類,只知支援T本身。

3.逆變

[-T], contravariant, 類似(? supers T) 只能用T的父類來替換T。是逆里氏替換原則。

上界:

只允許T的超類U來替換T。 [U >: T]

下界:

只允許T的子類U來替代T。 [U <: T]

理解概念後,以後讀原始碼的時候才不會不知雲。

-EOF-