Scala的協變covariant(+),逆變contravariant(-),上界(:)

原文:https://my.oschina.net/xinxingegeya/blog/486671

Scala的協變(+),逆變(-),上界(<:),下界(>:)

協變covariant、逆變contravariant、不可變invariant

對於一個帶類型參數的類型,比如 List[T],如果對A及其子類型B,滿足 List[B]也符合List[A]的子類型,那麼就稱爲covariance(協變) ,如果 List[A]是 List[B]的子類型,即與原來的父子關係正相反,則稱爲contravariance(逆變)。

如果一個類型支持協變或逆變,則稱這個類型爲variance(翻譯爲可變的或變型),否則稱爲invariance(不可變的)  

在Java裏,泛型類型都是invariant,比如 List<String> 並不是 List<Object> 的子類型。

而scala支持,可以在定義類型時聲明(用加號表示爲協變,減號表示逆變),如: 

trait List[+T] // 在類型定義時聲明爲協變

這樣會把List[String]作爲List[Any]的子類型。

不過Java支持使用(use-site variance),也就是在聲明變量時

List<? extends Object> list = new ArrayList<String>();

如下面的代碼示例

List<String> aList = new ArrayList<String>();
List<? extends Object> covariantList = aList;
List<? super String> contravariantList = aList;
covariantList.add("d"); //wrong
Object a = covariantList.get(0);

contravariantList.add("d"); //OK
String b = contravariantList.get(1); //wrong
Object c = contravariantList.get(2);

Java的上界和下界

使用extends關鍵字確定參數化類型的上界

在這裏參數化類型是Date,也有可能是Date的子類,所以add方法受限制

@Test
public void upperBound(List<? extends Date> list, Date date) {
  Date now = list.get(0);
  System.out.println("now==>" + now);
  //list.add(date); //這句話無法編譯,實際調用時傳入的list可能是java.util.Date的某個子類的參數化類型
  list.add(null);//這句可以編譯,因爲null沒有類型信息
}

使用super關鍵字確定參數化類型的下界

在這裏,參數化類型有可能是Timestamp,有可能是其父類(包括Object)

public void lowerBound(List<? super Timestamp> list) {
  Timestamp now = new Timestamp(System.currentTimeMillis());
  list.add(now);
  //Timestamp time = list.get(0); //不能編譯。方法返回的對象類型可能是Date甚至是Object,不能安全的向下轉換到Timestamp,也就因此無法編譯了
}

Scala的協變和逆變

object app_main_1 extends App {
  val t: Temp[Super] = new Temp[Sub]("hello world")
  print(t.toString)
}

class Temp[+A](title: String)

//支持協變

class Super

class Sub extends Super

但要注意的是,variance(變型)並不會被繼承,父類聲明爲variance(可變類型),子類如果想要保持,仍需要聲明成可變類型

如下所示,

scala> trait A[+T]
defined trait A

scala> class C[T] extends A[T]
defined class C

scala> class X; class Y extends X;
defined class X
defined class Y

scala> val c:C[X] = new C[Y]
<console>:11: error: type mismatch;
  found   : C[Y]
  required: C[X]
  Note: Y <: X, but class C is invariant in type T.
  You may wish to define T as +T instead. (SLS 4.5)
  val c:C[X] = new C[Y]
  ^

  scala>

You may wish to define T as +T instead. (SLS 4.5)

應該寫成下面這樣,

scala> class C[+T] extends A[T]
defined class C

scala> val c:C[X] = new C[Y]
c: C[X] = C@7241a47d

scala>

Scala逆變和協變的實例分析

在Scala(以及其他許多編程語言)中,函數也是對象,可以使用、定義其他對象的地方,也可以使用、定義函數。Scala中的函數,具有apply方法的類的實例,就可以當做函數來使用。其中apply接受的參數就是函數的參數,而apply的返回值就是函數的返回值。

首先給出一個接受一個參數的函數的泛型定義

trait Function1[-T, +U] {
  def apply(x: T): U
}

這種函數接受一個參數,參數類型爲泛型類型T,返回類型爲泛型類型U。和其他支持泛型的語言一樣,實際定義函數時T和U的類型會被確定下來,不過需要注意的是,這邊的T之前有一個“-”,而U之前有一個“+”。

在這裏引入關於這個符號的說明,在聲明Scala的泛型類型時,“+”表示協變,而“-”表示逆變 

  1. C[+T]:如果A是B的子類,那麼C[A]是C[B]的子類。>>>>>協變 

  2. C[-T]:如果A是B的子類,那麼C[B]是C[A]的子類。>>>>>逆變 

  3. C[T]:無論A和B是什麼關係,C[A]和C[B]沒有從屬關係。

根據Liskov替換原則,如果A是B的子類,那麼能適用於B的所有操作,都適用於A。讓我們看看這邊Function1的定義,是否滿足這樣的條件。假設Bird是Animal的子類,那麼看看下面兩個函數之間是什麼關係:

def f1(x: Bird): Animal // instance of Function1[Bird, Animal]

def f2(x: Animal): Bird // instance of Function1[Animal, Bird]

在這裏f2的類型是f1的類型的子類。爲什麼?

我們先看一下參數類型,根據Liskov替換原則,f1能夠接受的參數,f2也能接受 。在這裏f1接受的Bird類型,f2顯然可以接受,因爲Bird對象可以被當做其父類Animal的對象來使用。

再看返回類型,f1的返回值可以被當做Animal的實例使用,f2的返回值可以被當做Bird的實例使用,當然也可以被當做Animal的實例使用。

所以我們說,函數的參數類型是逆變的,而函數的返回類型是協變的。

函數的參數類型是逆變的,而函數的返回類型是協變的 

那麼我們在定義Scala類的時候,是不是可以隨便指定泛型類型爲協變或者逆變呢?答案是否定的。通過上面的例子可以看出,如果將Function1的參數類型定義爲協變,或者返回類型定義爲逆變,都會違反Liskov替換原則,因此,Scala規定,協變類型只能作爲方法的返回類型,而逆變類型只能作爲方法的參數類型。類比函數的行爲,結合Liskov替換原則,就能發現這樣的規定是非常合理的。

這種函數的泛型特性對於函數式編程非常有用。儘管C++的泛型在語法層面上不支持協變與逆變,但在C++11的function<U(T)>中,返回類型U和參數類型T也同樣遵循與Scala相同的協變與逆變規則。

注: 里氏替換原則中說,任何基類可以出現的地方,子類一定可以出現。 

參考:http://blog.csdn.net/oopsoom/article/details/24773239

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。是逆里氏替換原則。


Scala上界(<:)和下界(>:)

1) U >: T

這是類型下界的定義,也就是U必須是類型T的父類(或本身,自己也可以認爲是自己的父類)。

2) S <: T

這是類型上界的定義,也就是S必須是類型T的子類(或本身,自己也可以認爲是自己的子類)。

如何使用上界和下界,看一個綜合示例,

package com.usoft2

//出版物類
class Publication(val title: String)

//書籍類
class Book(title: String) extends Publication(title)

//圖書庫類
object Library {
  //定義圖書庫內所有的書籍
  val books: Set[Book] = Set(
    new Book("Programming in Scala"),
    new Book("Walden")
  )

  //打印所有圖書內容,使用外部傳入的函數來實現
  def printBookList(info: Book => AnyRef) {
    //確認Scala中一個參數的函數實際上是Function1特徵的實例
    assert(info.isInstanceOf[Function1[_, _]])
    //打印
    for (book <- books)
      println(info(book))
  }

  //打印所有圖書內容,使用外部傳入的GetInfoAction特徵的實例來實現
  def printBokkListByTrait[P >: Book, R <: AnyRef](action: GetInfoAction[P, R]) {
    //打印
    for (book <- books)
      println(action(book))
  }

}

//取得圖書內容特徵,P類型參數的類型下界是BookR類型參數的類型上界是AnyRef
trait GetInfoAction[P >: Book, R <: AnyRef] {
  //取得圖書內容的文本描述,對應()操作符
  def apply(book: P): R
}

//單例對象,文件的主程序
object Customer extends App {
  //定義取得出版物標題的函數
  def getTitle(p: Publication): String = p.title

  //使用函數來打印
  Library.printBookList(getTitle)

  //使用特徵GetInfoAction的實例來打印
  Library.printBokkListByTrait(new GetInfoAction[Publication, String] {
    def apply(p: Publication): String = p.title
  })
}

參考:

http://hongjiang.info/scala-covariance-and-contravariance/

http://fineqtbull.iteye.com/blog/477994

http://deltamaster.is-programmer.com/posts/48772.html?utm_source=tuicool

================================END================================


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章