Flutter Dart也支持泛型和泛型的协变与逆变,并且用起来比Java,Kotlin更方便。那么Dart中的泛型协变和逆变,应该如何理解和使用呢?它与Java,Kotlin中的逆变和协变又有什么区别呢?文章将从浅到深跟大家一起来探讨学习。
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); } } |
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}; |
在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中的协变,逆变和不变之前,我们先来搞清楚什么是类型关系。
类和类型的关系容易混淆。Dart中的类可分为两大类: 泛型类和非泛型类。
非泛型类是开发中接触最多的类。非泛型类去定义一个变量时,这个非泛型类就是这个变量的类型。例如:
泛型类比非泛型类复杂,一个泛型类可以对应无限种类型。在定义泛型类的时候会定义泛型形参,要拿到一个真实的泛型类型,就需要在使用泛型类的地方,给泛型类传入具体的类型实参替换定义中的类型形参。
我们经常使用的集合基本都会使用泛型,所以以集合举例说明,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的子类,它能代替Person出现的任何地方,所以它们之间存在子类型关系。而double不能替代int出现的地方,所以它们不存在子类型关系。但子类型关系,不一定是子类关系。
介绍完泛型和子类型关系概念,下面开始具体介绍类型关系变换:协变,逆变和不变。我们下面以Java为示例进行概念说明,在Java代码中使用<? extends T> 和 <? super T> 来表达协变与逆变(在Kotlin中用<out T> 和 <in T> 表示 )。
协变,逆变和不变是用来描述类型转换后的类型关系,其定义如下:
如果A,B表示类型,f(*)表示类型转换,< 表示子类型关系(比如,A<B表示A是B的子类型),则:
上面的定义看起来比较抽象,下面以Java中的ArrayList集合举例说明:
为什么要提出上面的协变与逆变概念呢?其意义就是为了让泛型实现多态。
我们知道,多态能将子类/实现类的对象赋值给父类/父接口,从而实现相同引用类可以指向不同实现体。比如PrintMsg(num i),参数可以传入int,double,num,这样就能用一个方法实现不同数据类型的相似功能。
那么泛型不能实现多态吗?因为泛型使用场景会牵扯到两种类型Class1<Class2>,所有要分情况来说:
因此,范型结合体不支持多态,而逆变和协变为这种场景提供了支持多态的解决方案。
首先,在Java中,普通泛型是“不变”的,即不支持多态,以ArrayList<E>举例说明:
Integer是Number的子类,如果可以把list2赋值给list1,则二者都指向了new ArrayList<Integer>()。这时,list1.get取数据没问题,因为里边都是Integer的对象。但是如果list1.add(Double),从list1的角度来说是正确的,因为Double也是Number类型。但是从list2的角度来说就是错误的,因为Double肯定不是Integer类型,list2取出一个Double类型的数据,这是破坏语言基本规则的,肯定不能通过编译。
所以普通泛型只能是不变的,即其普通泛型结合体不支持多态。
那如何解决普通泛型不支持多态的问题呢?
上面举例中,如果普通泛型支持多态,写入会导致问题。那通过限制不允许写入,就可以解决这个问题,如下:
上面<? extends Number>其实就是实现了泛型协变,即:
因为list1 只能读不能写,所以能保证上面的泛型协变,实现了范型结合体的多态。
上面的范型协变,是写限制的范型,那可不可以即支持多态还不限制写呢?
通过限制范型不允许读,就可以解决这个问题,如下:
首先,任何范型只能写入声明的类及其子类的对象才不会出错。上面list1 = list2,list1和list2相当于都指向了ArrayList<Object>,list1.add的都是Number及其子类的对象,也肯定是Number父类的子类,所以list.add不会出错。
但是如果list2.add过Object的对象,list1.get读出来的就不是Number的对象了,这肯定是破坏语言基本规则的,所以此时的多态,就通过限制范型不允许读来避免问题。
上面<? super Number>其实就是实现了泛型逆变,即:
因为list1 只能写不能读,所以能保证上面的泛型逆变,也实现了范型结合体的多态。
上面了解完泛型结合体的多态实现,接下来我们就要正确运用协变和逆变,这需要对其使用的场景有充分的理解。
协变很好理解,和我们常用的多态场景是类似的。协变只能读取数据,不能添加数据,所以只能作为生产者,向外提供数据,不能向它写入数据。
逆变就不太好理解,其难以理解的点就在于,一个超类型,数据更泛化(可能泛化到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) } |
通过上面的例子,我们可以看出来:
把只能从中读取的对象称为生产者(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只用了简单少量的代码,就支持了无数种事件类型的发布和订阅处理。
实际上在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页面。
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中泛型声明默认是支持协变的,不需要额外声明。但是,在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