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中的協變。我們最後總結下文章的核心內容:
- 泛型是為了功能復用,減少模板代碼而提出來的一種設計思想。
2.子類關係是一種繼承關係,子類型關係則是一種多態關係。子類關係一般是子類型關係,但子類型關係不一定是子類關係。
3.泛型的逆變,協變和不變,描述的是類型轉換關係,是為了泛型結合體支持多態而提出來的。PECS是其最佳的實踐原則。
4.Dart的新版本不支持逆變,支持協變,且默認支持協變。但是Dart的協變支持寫操作,有安全隱患,使用時需要額外注意。
作者:狐友陳金鳳
來源:微信公眾號:搜狐技術產品
出處:https://mp.weixin.qq.com/s/Vyl51PtpBQ_lCZR_uUMgZg