Flutter Dart:泛型的協變與逆變

閃念基因 發佈 2024-03-14T01:30:47.002868+00:00

Flutter Dart也支持泛型和泛型的協變與逆變,並且用起來比Java,Kotlin更方便。set msg { this._msg = msg;

Flutter Dart也支持泛型和泛型的協變與逆變,並且用起來比Java,Kotlin更方便。那麼Dart中的泛型協變和逆變,應該如何理解和使用呢?它與Java,Kotlin中的逆變和協變又有什麼區別呢?文章將從淺到深跟大家一起來探討學習。

一、Dart泛型

Dart中的泛型和其他語言的泛型一樣,都是為了減少大量的模板代碼,舉例說明:

// Dart

//這是一個列印int類型msg的PrintMsg

class PrintMsg {

int _msg;


set msg(int msg) { this._msg = msg;}

void printMsg() { print(_msg);}

}

當列印需求發生變化,需要支持列印更多種類的數據類型,不支持范型的話,代碼會大量增加,如下面這樣:

// Dart

//非范型寫法,現在需要新增支持String,double和自定義類的Msg,

class Msg {

@override

String toString() { return "This is Msg";}

}


class PrintMsg {

int _intMsg;

String _stringMsg;

double _doubleMsg;

Msg _msg;


set intMsg(int msg) { this._intMsg = msg;}

set stringMsg(String msg) { this._stringMsg = msg;}

set doubleMsg(double msg) { this._doubleMsg = msg;}

set msg(Msg msg) {this._msg = msg;}


void printIntMsg() { print(_intMsg);}

void printStringMsg() { print(_stringMsg);}

void printDoubleMsg() { print(_doubleMsg);}

void printMsg() { print(_msg);}

}

而有范型的支持後,不管增加多少種數據類型,列印類都可以簡化成如下幾行:

// Dart

//泛型寫法,簡化成幾行代碼,且支持無數種數據類型:

class PrintMsg<T> {

T _msg;


set msg(T msg) { this._msg = msg; }

void printMsg() { print(_msg); }

}

1、泛型類型省略

Dart中可以指定實際的泛型參數類型,也可以省略。實際上,編譯器會自動進行類型推斷,把泛型參數類型轉為dynamic類型。舉例說明:

// Dart

List<int> numTest = [1, 2, 3]; //注意int在Dart中是個類,繼承自num類。和Java中的基礎類型int不一樣,java中的int是不能作為泛型的實參的,因為int不是Object的子類。

Map<String, int> mapTest = {'a': 1, 'b': 2, 'c': 3};


//Dart可簡寫成如下形式, 但非常不推薦,因為泛型簡寫會被自動類型推斷為dynamic(非泛型簡寫的類型推斷不會變成dynamic)。

List numTest = [1, 2, 3];

Map mapTest = {'a': 1, 'b': 2, 'c': 3}

print(numTest.runtimeType.toString()); // output「List<dynamic>」

print(mapTest.runtimeType.toString()); // output「_InternalLinkedHashMap<dynamic,dynamic>」


//所以Dart的簡寫方式,相當於如下形式

List<dynamic> numTest = [1, 2, 3];

Map<dynamic, dynamic> mapTest = {'a': 1, 'b': 2, 'c': 3};


2、真偽泛型

在Java中,ArrayList是支持泛型的,但是它的數據存儲用的卻是Object[],這是因為Java在編譯的時候會進行類型擦除,也可以說Java中的泛型是種偽泛型,泛型只存在編譯時期,運行時泛型就會被擦除(所以運行時無法獲取泛型T的真實類型信息)。

Kotlin最終也會編譯生成和Java相同規格的class文件,所以Kotlin中的泛型也會被擦除,也無法使用{a is T;}進行類型判斷。

不過Kotlin為泛型的類型判斷做了一點改進,支持在inline函數裡判斷reified修飾的泛型類型。它的原理是,在編譯過程中,編譯器會將inline內聯函數的代碼替換到實際調用的地方,並且對reified定義的泛型參數,不進行泛型擦除,而把調用方的形參直接替換成具體的實參類型,這樣編譯的結果,就支持inline內聯函數內部對reified泛型進行類型判斷,舉例說明:

// Kotlin

inline fun <reified T> PrintMsg(value:T) {

LogUtil.d("test", T::class.toString()) // 非inline函數的reified泛型,不能調用T::class

LogUtil.d("test", value!!::class.toString()) //不管是否inline函數,都會列印參數的真實類型信息


var b = value is List<*> // * 不能換成任何具體類型,編譯器會報錯。注意,PrintMsg方法裡的泛型T,不是List<E>中的泛型E, E在Kotlin中運行時都會被擦除成為Object了。

LogUtil.d("test", b.toString())


b = "abc" is T // 非inline函數的reified泛型,則不能調用 is T

LogUtil.d("test", b.toString())

}


fun main() {

PrintMsg("test")

PrintMsg(arrayListOf(1, 2, 3))

PrintMsg(arrayListOf("a", "b", "c"))

}


// output

//D/test: class java.lang.String // 在inline函數裡,泛型T直接換成了實參類型String

//D/test: class java.lang.String // 讀取的是實參的真實類型信息。

//D/test: false

//D/test: true

//D/test: class java.util.ArrayList //泛型T換成了實參ArrayList,ArrayList中的元素E已經都換成了Object

//D/test: class java.util.ArrayList //此處ArrayList中的元素E已經都換成了Object

//D/test: true

//D/test: false

//D/test: class java.util.ArrayList //泛型T換成了實參ArrayList,此處ArrayList中的元素E已經都換成了Object

//D/test: class java.util.ArrayList //此處ArrayList中的元素E已經都換成了Object

//D/test: true

//D/test: false


而Dart的泛型是真泛型,在編譯期和運行期都可以通過泛型拿到其真實類型,所以Dart中可以直接用泛型進行類型判斷,用代碼舉例說明 :

// Dart

void PrintMsg<T>(T value) {

print("test---"+ T.toString()); //使用沒有限制,可以獲取List<E>中的泛型E的具體類型

print("test---"+ value.runtimeType.toString());


var b = value is List<String>; // 可以直接判斷范型結合體的具體類型

print("test---2 "+ b.toString());


b = "abc" is T; //使用沒有限制,T就是一種類型

print("test---3 "+ b.toString());

}


void main() {

PrintMsg("abc");

PrintMsg(List<int>.from({1,2,3}));

PrintMsg(List<String>.from({"a","b","c"}));

}


// output

//I/flutter (21231): test---String

//I/flutter (21231): test---String

//I/flutter (21231): test--- false

//I/flutter (21231): test--- true

//I/flutter (21231): test---List<int> //此處List中的E元素仍然是int對象

//I/flutter (21231): test---List<int> //此處List中的E元素仍然是int對象

//I/flutter (21231): test--- false

//I/flutter (21231): test--- false

//I/flutter (21231): test---List<String> //此處List中的E元素仍然是String對象

//I/flutter (21231): test---List<String> //此處List中的E元素仍然是String對象

//I/flutter (21231): test--- true

//I/flutter (21231): test--- false


通過示例代碼和output log,我們可以看出,泛型是編譯期的一個概念,在運行期,Java/Kotlin 把泛型都轉換成了Object,而Dart保留了具體的實參類型,都不再是一個不確定的形參類型。

二、泛型關係:協變,逆變和不變

協變,逆變和不變是一種描述泛型類型關係變化的概念。在了解Dart中的協變,逆變和不變之前,我們先來搞清楚什麼是類型關係

1、類和類型,子類和子類型

類和類型的關係容易混淆。Dart中的類可分為兩大類: 泛型類和非泛型類

非泛型類是開發中接觸最多的類。非泛型類去定義一個變量時,這個非泛型類就是這個變量的類型例如:

  • 定義一個非泛型類 Class Person,那麼就會有個Person類型。
  • 定義一個非泛型類 Class Boy extend Person類,會有個Boy類型

泛型類比非泛型類複雜,一個泛型類可以對應無限種類型。在定義泛型類的時候會定義泛型形參,要拿到一個真實的泛型類型就需要在使用泛型類的地方,給泛型類傳入具體的類型實參替換定義中的類型形參。

我們經常使用的集合基本都會使用泛型,所以以集合舉例說明,List<E>是一個類,它不是一個類型,它可以衍生成無限種類型。例如,List<String>, List<int>,List<List<int>>都是不同的類型。

所以,每個非泛型類會對應一個類型。而每個泛型類,會對應無限種類型。

知道了類和類型的關係,我們再來看子類和子類型的關係。

子類就是派生類,它繼承父類。例如: class Boy extends Person,則Boy就是Person的子類,Person 是 Boy類的父類

子類型沒有繼承關係,它的定義是: 需要A類型值的任何地方,都可以使用B類型的值來替換,則B類型就是A類型的子類型,或A類型是B類型的超類型。‍

舉例說明:

  • Boy是Person的子類和子類型。

  • List<E>類可以衍生出很多類型,這些類型之間都不是子類關係,但可能是子類型關係。如 List<int>不是List<num>的子類,但是List<int>是List<num>的子類型(Dart默認支持協變,後面會講)。
  • List<E>是Iterable<E>的子類,但因為List<E>不是一種類型,所以List<E>和Iterable<E>不是子類型關係。


所以屬於子類關係的類型,也會成為子類型關係。例如:Boy是Person的子類,它能代替Person出現的任何地方,所以它們之間存在子類型關係。而double不能替代int出現的地方,所以它們不存在子類型關係。但子類型關係,不一定是子類關係。

介紹完泛型和子類型關係概念,下面開始具體介紹類型關係變換:協變,逆變和不變。我們下面以Java為示例進行概念說明,在Java代碼中使用<? extends T> 和 <? super T> 來表達協變與逆變(在Kotlin中用<out T> 和 <in T> 表示 )。

2、類型關係的定義與意義

協變,逆變和不變是用來描述類型轉換後的類型關係,其定義如下:

如果A,B表示類型,f(*)表示類型轉換,< 表示子類型關係(比如,A<B表示A是B的子類型),則:

  • f(*)是協變(covariant)的,當A<B時有f(A)<f(B)成立;
  • f(*)是逆變(contravariant)的,當A<B時有f(B)<f(A)成立;
  • f(*)是不變(invariant)的,當A<B時,f(A)與f(B)相互之間沒有繼承關係。


上面的定義看起來比較抽象,下面以Java中的ArrayList集合舉例說明:

  • ArrayList<? extends Number>這種形式的泛型是支持協變的,它可以被賦值為ArrayList<Number>、ArrayList<Integer>,但是不能被賦值為ArrayList<Object>
  • ArrayList<? super Number>這種形式的泛型是支持逆變的,它可以被賦值為ArrayList<Number>、ArrayList<Object>,但是不能被賦值為ArrayList<Integer>
  • ArrayList<Number>這種形式的泛型是不變的,就是說ArrayList<Number> list,不能被賦值為ArrayList<Integer>,也不能被賦值為ArrayList<Object>,只能被賦值為ArrayList<Number>


為什麼要提出上面的協變與逆變概念呢?其意義就是為了讓泛型實現多態

我們知道,多態能將子類/實現類的對象賦值給父類/父接口,從而實現相同引用類可以指向不同實現體。比如PrintMsg(num i),參數可以傳入int,double,num,這樣就能用一個方法實現不同數據類型的相似功能。

那麼泛型不能實現多態嗎?因為泛型使用場景會牽扯到兩種類型Class1<Class2>,所有要分情況來說:

  • 對於Class1來說,是支持多態的,比如Collection<Integer> collect = new ArrayList<Integer>();

  • 對於Class2來說,有兩個維度去理解:

  • (1)Class2本身是支持多態的,比如ArrayList<Integer)、 list.add(Double)
  • Number> list, 這個list可以add任何Number子類的對象,如 list.add(
  • (2)Class1<Class2>結合體是不支持多態的,比如ArrayList<Number> list = new ArrayList<Integer>(),這個是不對的(注意,Java泛型結合體是不變的,默認是不支持協變的)。

因此,范型結合體不支持多態,而逆變和協變為這種場景提供了支持多態的解決方案。

3、多態的實現和限制

首先,在Java中,普通泛型是「不變」的,即不支持多態,以ArrayList<E>舉例說明:

ArrayList<Number> list1 = new ArrayList<Number>();

ArrayList<Integer> list2 = new ArrayList<Integer>();

list1 = list2;// 編譯錯誤!

Integer是Number的子類,如果可以把list2賦值給list1,則二者都指向了new ArrayList<Integer>()。這時,list1.get取數據沒問題,因為裡邊都是Integer的對象。但是如果list1.add(Double),從list1的角度來說是正確的,因為Double也是Number類型。但是從list2的角度來說就是錯誤的,因為Double肯定不是Integer類型,list2取出一個Double類型的數據,這是破壞語言基本規則的,肯定不能通過編譯。

所以普通泛型只能是不變的,即其普通泛型結合體不支持多態。

那如何解決普通泛型不支持多態的問題呢?

上面舉例中,如果普通泛型支持多態,寫入會導致問題。那通過限制不允許寫入,就可以解決這個問題,如下:

ArrayList<? extends Number> list1 = new ArrayList<Number>();

ArrayList<Integer> list2 = new ArrayList<Integer>();

list1 = list2;

Number a = list1.get(0);

list1.add(1) // 編譯錯誤!


上面<? extends Number>其實就是實現了泛型協變,即:

Integer < Number

ArrayList<Integer> < ArrayList<? extends Number>

因為list1 只能讀不能寫,所以能保證上面的泛型協變,實現了范型結合體的多態。

上面的范型協變,是寫限制的范型,那可不可以即支持多態還不限制寫呢?

通過限制范型不允許讀,就可以解決這個問題,如下:

ArrayList<? super Number> list1 = = new ArrayList<Number>();

ArrayList<Object>list2=new ArrayList<Object>();

list1 = list2;

Number a = list1.get(0);// 編譯錯誤!

list1.add(1);

首先,任何范型只能寫入聲明的類及其子類的對象才不會出錯。上面list1 = list2,list1和list2相當於都指向了ArrayList<Object>,list1.add的都是Number及其子類的對象,也肯定是Number父類的子類,所以list.add不會出錯。

但是如果list2.add過Object的對象,list1.get讀出來的就不是Number的對象了,這肯定是破壞語言基本規則的,所以此時的多態,就通過限制范型不允許讀來避免問題。

上面<? super Number>其實就是實現了泛型逆變,即:

Number < Object

ArrayList<? super Number> > ArrayList<Object>


因為list1 只能寫不能讀,所以能保證上面的泛型逆變,也實現了范型結合體的多態。

4、協變和逆變的應用和實踐

上面了解完泛型結合體的多態實現,接下來我們就要正確運用協變和逆變,這需要對其使用的場景有充分的理解。

協變很好理解,和我們常用的多態場景是類似的。協變只能讀取數據,不能添加數據,所以只能作為生產者,向外提供數據,不能向它寫入數據。

逆變就不太好理解,其難以理解的點就在於,一個超類型,數據更泛化(可能泛化到Object),那不是做不了什麼嗎?如果把逆變的場景換到參數的函數功能復用,而非參數的數據使用,就能更好理解了。

協變和逆變在集合中都有廣泛的運用,所有我們繼續以Java的ArrayList為例,舉例說明:

// Java

public class ArrayList<E> extends AbstractList<E>

implements List<E>, RandomAccess, Cloneable, java.io.Serializable {


transient Object[] elementData;

private int size;

// 省略代碼......


public boolean addAll(Collection<? extends E> c) { // c參數支持協變

Object[] a = c.toArray(); // 對c進行讀操作

int numNew = a.length;

ensureCapacityInternal(size + numNew);

System.arraycopy(a, 0, elementData, size, numNew); // 寫入到elementData數組中

size += numNew;

return numNew != 0;

}


public void forEach(Consumer<? super E> action) {// action參數支持逆變

Objects.requireNonNull(action);

final int expectedModCount = modCount;

@SuppressWarnings("unchecked")

final E[] elementData = (E[]) this.elementData;

final int size = this.size;

for (int i=0; modCount == expectedModCount && i < size; i++) {

action.accept(elementData[i]); // 對action寫操作

}


if (modCount != expectedModCount) {

throw new ConcurrentModificationException();

}

}

}


class Person {

String name;

char sex;


Person(String n, char s) {

name = n;

sex = s;

}

}


class Boy extends Person {

Boy(String n) {

super(n, "male");

}

}


class Girl extends Person {

Girl(String n) {

super(n, "female");

}

}


public void main() {

ArrayList<Person> array1 = new ArrayList<Person>();

array1.add(new Person("p1b", "male"));

array1.add(new Person("p1g", "female"));


ArrayList<Boy> array2 = new ArrayList<Boy>();

array2.add(new Boy("b1"));

array2add(new Boy("b2"));


// Boy是Person的子類型,所以Collection<Boy>也是Collection<?extends Person>的子類型,

// 而ArrayList繼承自Collection,所以ArrayList<Boy>是Collection<Boy>的子類型,也同樣是Collection<?extends Person>的子類型,

// 然後,array1的addAll參數支持Collection<?extends Person>泛型協變,

// 所以,array1的addAll參數可以傳入array2.

array1.addAll(array2);


int count = 0;

Consumer<Person> comsumerPerson = new Consumer<Person>(){

@Override

public void accept(Person person) {

if(person.name.contain("g")) {

// do some action

count++;

}

}

};


// array1的forEach支持Consumer<? super Person>泛型逆變,

// Consumer<Person>參數屬於本來類型,默認支持。

array1.forEach(comsumerPerson)


ArrayList<Girl> array3 = new ArrayList<Girl>();

array3.add(new Girl("g1"));

array3.add(new Girl("g2"));


// array3的forEach參數支持Consumer<? super Girl>泛型逆變,

// Person是Girl的超類型,所以Consumer<Person>是Consumer<? super Girl>的子類型,

// 所以,comsumerPerson可以作為forEach的參數。

array3.forEach(comsumerPerson)

}


通過上面的例子,我們可以看出來:

  • addAll函數中的參數c是作為生產者,從自身讀取元素提供給ArrayList。通過協變參數Collection<? extends E> 中讀取的元素肯定是ArrayList中元素E的子類型,子類型放進數組中肯定是支持的。
  • forEach函數中的參數action則是作為消費者,從ArrayList拿取元素提供給給自己使用。在array3.forEach(comsumerPerson) 中,Consumer<Person>是Consumer<? super Girl>的子類型,可以作為參數傳入,comsumerPerson拿到array3的Girl元素,在accept函數中統計Person名字含「g」的人員數。這樣comsumerPerson的函數功能就得到了很好的復用。


把只能從中讀取的對象稱為生產者(Producer),只能寫入的對象稱為消費者(Consumer),即只能從Producer中get對象,只能put對象給Consumer,這就是著名的PECS原則(Producer-Extends, Consumer-Super),也是協變和逆變的最佳實踐原則。

在狐友的代碼中也通過使用協變和逆變,使得通用類可以適配大量的數據類型。以單例的事件總線LivedataBus為例說明:

事件總線bus通過BusMutableLiveData<out BusEvent>聲明支持協變,這樣就可以在總線bus中添加所有BusEvent子類型事件。比如BusMutableLiveData<TeamUpPublishEvent> (TeamUpPublishEvent 繼承BusEvent),BusMutableLiveData<TeamUpSearchResultEvent>。

事件總線bus通過Event的類型名稱作為key進行事件發布和訂閱的匹配橋樑,讓發布的事件可以通知到所有訂閱者。

BusMutableLiveData的observe方法,通過Observer<in T>聲明參數支持逆變,讓在onChange處理相似數據類型的Observer,可以有機會被多次復用。

而BusMutableLiveData作為可感知生命周期,可觀察數據變化的類型,用它來包裝總線事件BusEvent,可以方便業務訂閱各種事件通知, 如訂閱組局發布事件 TeamUpPublishEvent,組局搜索事TeamUpSearchResultEvent :

這裡,TeamUpPublishEvent的事件訂閱者,會在onChanged收到事件時,先判斷發布成功才會刷新UI。TeamUpSearchResultEvent的事件訂閱者,則會在onChanged收到事件時上報搜索的結果。

所以,通過運用協變和逆變,事件總線LivedataBus只用了簡單少量的代碼,就支持了無數種事件類型的發布和訂閱處理。

三、Dart協變

實際上在Dart1.x的版本種是既支持協變又支持逆變,但是在Dart2.x版本開始僅支持協變。所以後面我們就不再討論Dart的逆變。

在Dart中所有泛型類都默認支持協變(類似Java的<?extend T>,Kotlin的<out T>),不需要像Java或Kotlin一樣,需要用額外的關鍵字聲明。

從上一節的分析可知,協變實際上就是泛型結合體保留了泛型參數的子類型化關係比如說int是num的子類型,所以List<int>就是List<num>的子類型,這就是結合體的子類型關係保留了泛型參數的子類型關係,用例子說明:

// Dart

class Person {

final String name;

final String sex;


Person(this.name, this.sex);

}


class Boy extends Person {

Boy(this.name) : super(this.name, "male");

}


class Girl extends Person {

Girl(this.name) : super(this.name, "female");

}


void PrintMsg(List<Person> persons) { //根據多態的概念,persons可以傳入List<Person> or 其子類型的對象。

for (var person in persons) {

print('${persons.name}---${persons.sex}');

}

}


void main() {

List<Girl> girls = [];

girls.add(Girl("g1"));

// Girl是Person的子類型,List支持協變,則List<Girl>是List<Person>子類型。

// 而PrintMsg函數接收一個List<Person>類型,根據協變的原則,可以使用子類型代替超類型,可以使用List<Girl>類型替代。

PrintMsg(girls);


List<Boy> boys = [];

boys.add(Boy("b1"));

PrintMsg(boys);// 同上,List<Boy>也是List<Person>的子類型


List<Person> persons = [];

persons.add(Person("p1","female"));

persons.add(Person("p2","male"));

PrintMsg(persons);// List<Person>自身,也是可以的。

}


從上面例子可以看出,在Dart中泛型聲明默認是支持協變的,不需要像Java那樣在代碼中額外聲明<? extends Person>。

我們在Flutter項目中,也有大量的泛型協變的應用,以實際代碼舉例:

在buildBody方法中,接受一個List<Widget>參數進行Page構建。

再看調用的位置,實際上傳參是一個通過buildButton返回的ElevatedButton(繼承自Widget)構建出來的一個List<ElevatedButton>,如下所示:

因為List<Widget>默認支持協變,所以buildBody方法可以接受一個List<ElevatedButton>類型的實參。buildBody方法正是通過協變,支持了更多種類的頁面數據,從而構建起item不同,布局類似的page頁面。

四、Dart協變安全

Java,Kotlin中協變和逆變都是安全的,但是Dart的泛型型變是存在安全問題的,因為Dart中的協變是支持讀寫的,而Java,Kotlin中協變是不支持寫的。

以List<T>集合為例,它在Java和Kotlin中都是不變的。

Java通過聲明List<? extend T>變量支持協變,並限制其數據T只能讀不能寫,從而保證了泛型協變安全

Kotlin和Java一樣,也通過聲明List<out T>來支持協變並限制其數據只讀。但Kotlin對集合做了進一步優化,通過把集合分為可讀寫集合MutableList<E>只讀集合List<out E>來保證安全問題。其中MutableList<E>是可讀可寫,但不支持協變和逆變,而List<out E>只讀,且支持協變。

Dart中,List<T>默認支持協變,又是可讀可寫的,這樣就會存在安全問題,我們舉例說明:

// Dart

class Person {

final String name;

final String sex;


Person(this.name, this.sex);

}


class Boy extends Person {

Boy(this.name) : super(this.name, "male");

}


class Girl extends Person {

Girl(this.name) : super(this.name, "female");

}


void PrintMsg(List<Person> persons) { //根據多態的概念,persons可以傳入List<Person> or 其子類型的對象。但是,這裡的List<Person>實際是不安全的!!!

for (var person in persons) {

print('${persons.name}---${persons.sex}');

}

}


void main() {

List<Girl> girls = [];

girls.add(Girl("g1"));

PrintMsg(girls); // Girl是Person的子類型,List支持協變,則List<Girl>是List<Person>子類型。

}

上面代碼編譯和運行都沒有問題,但是PrintMsg中的List<Person>參數實際是不安全的!!!我們在上面的PrintMsg中對參數進行寫操作,如下:

// Dart

void PrintMsg(List<Person> persons) {

//根據多態的概念,persons可以傳入List<Person> or 其子類型的對象。但是,這裡的List<Person>類型,因為可寫,實際上是不安全的!!!


// 在Dart1.x版本中運行也是可以通過的,原因不做探究。但是後續main通過girls讀出一個Boy來也會出錯!

// 在Dart2.x版本中運行是會報錯的:type 'Boy' is not a subtype of type 'Girl'

// 在Dart中,List都是可讀寫的,協變的。看似在List<Person>中添加Boy,實際上是在List<Girl>中添加Boy。如果後續從List<Girl>中讀出一個Boy來,這是不允許的。

persons.add(Boy("b999")); // 編譯通過,但運行報錯!!!


for (var person in persons) {

print('${persons.name}---${persons.sex}');

}

}


main() {

List<Girl> girls = [];

girls.add(Girl("g1"));

PrintMsg(girls); // Girl是Person的子類型,List支持協變,則List<Girl>是List<Person>子類型。

}


而在Kotlin中的不會存在上面那種問題。Kotlin中集合分為可讀寫集合MutableList<E>只讀集合List<out E>, 舉例說明:

// Kotlin

fun PrintMsg(persons: List<Person>) {

// Kotlin中List支持協變,且只讀。這樣的泛型類型是具有安全性的。


//因為在Kotlin中List被定義為只讀集合List<out E>,

//沒有add, remove等寫操作方法,

//所以List<Person>中是無法添加一個Boy的。

persons.add(Boy("b1")) // 此處編譯不通過。


for (person in persons) {

println(person.name + "---" + person.sex)

}

}


fun main() {

val girls = listOf(Girl("g1"))


// Girl是Person的子類型,List支持協變,則List<Girl>是List<Person>子類型,

// 可以作為PrintMsg的參數。

PrintMsg(girls)

}

在Dart中使用協變List很方便,但是發生協變的情況下,需要注意寫操作的控制。

五、Dart協變關鍵字convariant

上面說到,在Dart中泛型聲明默認是支持協變的,不需要額外聲明。但是,在Dart中有個協變關鍵字convariant,它又有什麼作用呢?舉例說明 :

// Dart

class Person {

final String name;

final String sex;


Person(this.name, this.sex);

}


class Boy extends Person {

Boy(this.name) : super(this.name, "male");

}


class Girl extends Person {

Girl(this.name) : super(this.name, "female");

}


class House { //宿舍

void checkin(Person person) {

print('checkin ${persons.name}---${persons.sex}');

}

}


class BoyHouse extends House { //男宿舍

@override

void checkin(Person person) { // 因為是重寫的checkin方法,參數需要和父類保持一致

super.checkin(person);

}

}


class GirlHouse extends House { //女宿舍

@override

void checkin(Person person) { // 因為是重寫的checkin方法,參數需要和父類保持一致

super.checkin(person);

}

}


void main() {

var bh = BoyHouse();

bh.checkin(Boy("b1"));

bh.checkin(Girl("g1")); //把女孩安排到男宿舍!!!編譯運行都不會有問題!


var gh = GirlHouse();

gh.checkin(Girl("g2"));

gh.checkin(Boy("b2"));//把男孩安排到女宿舍!!!編譯運行都不會有問題!

}

上面的例子中,我們發現可以把女孩安排到男宿舍住,男孩安排到女宿舍住。男女宿舍的劃分相當於形同虛設。

為了解決這個問題,Dart可以通過 covariant 協變關鍵字,讓重寫方法中的參數更具體,舉例說明:

// Dart

class BoyHouse extends House { //男宿舍

@override

void checkin(covariant Boy person) { // 重寫的checkin方法,但通過covariant協變關鍵字,限制參數為更具體的Person的子類Boy

super.checkin(person);

}

}


class GirlHouse extends House { //女宿舍

@override

void checkin(covariant Girl person) { // 重寫的checkin方法,但通過covariant協變關鍵字,限制參數為更具體的Person的子類Girl

super.checkin(person);

}

}



void main() {

var bh = BoyHouse();

bh.checkin(Boy("b1"));

bh.checkin(Girl("g1")); //編譯不通過!BoyHouse的checkin限制了參數只能是Boy類型


var gh = GirlHouse();

gh.checkin(Girl("g2"));

gh.checkin(Boy("b2"));//編譯不通過!GirlHouse的checkin限制了參數只能是Girl類型

}

在我們的摸魚項目中,也有大量的convariant運用場景,以_KKPageViewState子類代碼為例說明:

父類State中didUpdateWidget方法只能知道要通知StatefullWidget的更新,通過convariant關鍵字,可以在子類_KKPageViewState的didUpdateWidget方法中,限制其參數必須是KKPageView (KKPageView 是 StatefullWidget的子類,支持參數協變)。

六、總結

通過上面的學習,我們對Dart中的協變有了更深入的理解,開發過程中也能更好更安全地運用Dart中的協變。我們最後總結下文章的核心內容:

  1. 泛型是為了功能復用,減少模板代碼而提出來的一種設計思想。

2.子類關係是一種繼承關係,子類型關係則是一種多態關係。子類關係一般是子類型關係,但子類型關係不一定是子類關係。

3.泛型的逆變,協變和不變,描述的是類型轉換關係,是為了泛型結合體支持多態而提出來的。PECS是其最佳的實踐原則。

4.Dart的新版本不支持逆變,支持協變,且默認支持協變。但是Dart的協變支持寫操作,有安全隱患,使用時需要額外注意。

作者:狐友陳金鳳

來源:微信公眾號:搜狐技術產品

出處:https://mp.weixin.qq.com/s/Vyl51PtpBQ_lCZR_uUMgZg

關鍵字: