深入理解Flutter Dart:泛型的协变与逆变

发表时间: 2023-03-17 14:22

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