Flutter Dart:泛型的协变与逆变

Flutter Dart:泛型的协变与逆变_第1张图片

本文字数:16281 

预计阅读时间 41 分钟

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 _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的子类。

Mapint> 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

print(mapTest.runtimeType.toString());   // output“_InternalLinkedHashMap

//所以Dart的简写方式,相当于如下形式

List numTest = [1, 2, 3];

Map 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 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在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 value) {

  print("test---"+ T.toString());   //使用没有限制,可以获取List中的泛型E的具体类型

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

  var b = value is List;   // 可以直接判断范型结合体的具体类型

  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.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   //此处List中的E元素仍然是int对象

//I/flutter (21231): test---List   //此处List中的E元素仍然是int对象

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

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

//I/flutter (21231): test---List  //此处List中的E元素仍然是String对象

//I/flutter (21231): test---List  //此处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是一个类,它不是一个类型,它可以衍生成无限种类型。例如,List, List,List>都是不同的类型。

所以,每个非泛型类会对应一个类型。而每个泛型类,会对应无限种类型。

知道了类和类型的关系,我们再来看子类和子类型的关系。

子类就是派生类,它继承父类。例如: class Boy extends Person,则Boy就是Person的子类,Person 是 Boy类的父类

子类型没有继承关系,它的定义是: 需要A类型值的任何地方,都可以使用B类型的值来替换,则B类型就是A类型的子类型,或A类型是B类型的超类型。‍

举例说明:

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

  • List类可以衍生出很多类型,这些类型之间都不是子类关系,但可能是子类型关系。如 List不是List的子类,但是List是List的子类型(Dart默认支持协变,后面会讲)。

  • List是Iterable的子类,但因为List不是一种类型,所以List和Iterable不是子类型关系。

所以属于子类关系的类型,也会成为子类型关系。例如:Boy是Person的子类,它能代替Person出现的任何地方,所以它们之间存在子类型关系。而double不能替代int出现的地方,所以它们不存在子类型关系。但子类型关系,不一定是子类关系。

介绍完泛型和子类型关系概念,下面开始具体介绍类型关系变换:协变,逆变和不变。我们下面以Java为示例进行概念说明,在Java代码中使用 和 来表达协变与逆变(在Kotlin中用 表示 )。

2、类型关系的定义与意义

协变,逆变和不变是用来描述类型转换后的类型关系,其定义如下:

如果A,B表示类型,f(*)表示类型转换,< 表示子类型关系(比如,A

  • f(*)是

    协变

    (covariant)的,当A

  • f(*)是

    逆变

    (contravariant)的,当A

  • f(*)是

    不变

    (invariant)的,当A

上面的定义看起来比较抽象,下面以Java中的ArrayList集合举例说明:

  • ArrayList这种形式的泛型是

    支持协变的

    ,它可以被赋值为ArrayList、ArrayList,但是不能被赋值为ArrayList

    • ArrayList这种形式的泛型是

      支持逆变的

      ,它可以被赋值为ArrayList、ArrayList,但是不能被赋值为ArrayList

      • ArrayList这种形式的泛型是

        不变的

        ,就是说ArrayList list,不能被赋值为ArrayList,也不能被赋值为ArrayList,只能被赋值为ArrayList

        为什么要提出上面的协变与逆变概念呢?其意义就是为了让泛型实现多态

        我们知道,多态能将子类/实现类的对象赋值给父类/父接口,从而实现相同引用类可以指向不同实现体。比如PrintMsg(num i),参数可以传入int,double,num,这样就能用一个方法实现不同数据类型的相似功能。

        那么泛型不能实现多态吗?因为泛型使用场景会牵扯到两种类型Class1,所有要分情况来说:

        • 对于Class1来说,是支持多态的,比如

          Collection

          collect = new

           ArrayList

          ();

        • 对于Class2来说,有两个维度去理解:

        (1)Class2本身是支持多态的,比如ArrayList

        Number> list, 这个list可以add任何Number子类的对象,如 list.add(

        (2)Class1结合体是不支持多态的,比如ArrayList list = new ArrayList(),这个是不对的(注意,Java泛型结合体是不变的,默认是不支持协变的)。

        因此,范型结合体不支持多态,而逆变和协变为这种场景提供了支持多态的解决方案。

        3、多态的实现和限制

        首先,在Java中,普通泛型是“不变”的,即不支持多态,以ArrayList举例说明:

        ArrayList list1 = new ArrayList();

        ArrayList list2 = new ArrayList();

        list1 = list2;// 编译错误!

        Integer是Number的子类,如果可以把list2赋值给list1,则二者都指向了new ArrayList()。这时,list1.get取数据没问题,因为里边都是Integer的对象。但是如果list1.add(Double),从list1的角度来说是正确的,因为Double也是Number类型。但是从list2的角度来说就是错误的,因为Double肯定不是Integer类型,list2取出一个Double类型的数据,这是破坏语言基本规则的,肯定不能通过编译。

        所以普通泛型只能是不变的,即其普通泛型结合体不支持多态。

        那如何解决普通泛型不支持多态的问题呢?

        上面举例中,如果普通泛型支持多态,写入会导致问题。那通过限制不允许写入,就可以解决这个问题,如下:

        ArrayList list1 = new ArrayList();

        ArrayList list2 = new ArrayList();

        list1 = list2;

        Number a = list1.get(0);

        list1.add(1) // 编译错误!

        上面extends Number>其实就是实现了泛型协变,即:

        Integer  <  Number

        ArrayList  < ArrayList

        因为list1 只能读不能写,所以能保证上面的泛型协变,实现了范型结合体的多态。

        上面的范型协变,是写限制的范型,那可不可以即支持多态还不限制写呢?

        通过限制范型不允许读,就可以解决这个问题,如下:

        ArrayList list1 = = new ArrayList();

        ArrayListlist2=new ArrayList();

        list1 = list2;

        Number a = list1.get(0);// 编译错误!

        list1.add(1);

        首先,任何范型只能写入声明的类及其子类的对象才不会出错。上面list1 = list2,list1和list2相当于都指向了ArrayList,list1.add的都是Number及其子类的对象,也肯定是Number父类的子类,所以list.add不会出错。

        但是如果list2.add过Object的对象,list1.get读出来的就不是Number的对象了,这肯定是破坏语言基本规则的,所以此时的多态,就通过限制范型不允许读来避免问题。

        上面super Number>其实就是实现了泛型逆变,即:

        Number <  Object

        ArrayList  >  ArrayList

        因为list1 只能写不能读,所以能保证上面的泛型逆变,也实现了范型结合体的多态。

        4、协变和逆变的应用和实践

        上面了解完泛型结合体的多态实现,接下来我们就要正确运用协变和逆变,这需要对其使用的场景有充分的理解。

        协变很好理解,和我们常用的多态场景是类似的。协变只能读取数据,不能添加数据,所以只能作为生产者,向外提供数据,不能向它写入数据。

        逆变就不太好理解,其难以理解的点就在于,一个超类型,数据更泛化(可能泛化到Object),那不是做不了什么吗?如果把逆变的场景换到参数的函数功能复用,而非参数的数据使用,就能更好理解了。

        协变和逆变在集合中都有广泛的运用,所有我们继续以Java的ArrayList为例,举例说明:

        // Java

        public class ArrayList extends AbstractList

                implements List, RandomAccess, Cloneable, java.io.Serializable {

            transient Object[] elementData;

            private int size;

            // 省略代码......

            public boolean addAll(Collectionextends 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(Consumersuper 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 array1 = new ArrayList();

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

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

            ArrayList array2 = new ArrayList();

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

            array2add(new Boy("b2"));

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

           // 而ArrayList继承自Collection,所以ArrayList是Collection的子类型,也同样是Collection<?extends Person>的子类型,

           // 然后,array1的addAll参数支持Collection<?extends Person>泛型协变,

           // 所以,array1的addAll参数可以传入array2.

            array1.addAll(array2);  

            int count = 0;

         Consumer comsumerPerson = new Consumer(){

                  @Override

                  public void accept(Person person) {

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

                             // do some action

                             count++;

                       }

                  }

             };

            // array1的forEach支持Consumer泛型逆变,

           //  Consumer参数属于本来类型,默认支持。

            array1.forEach(comsumerPerson)

            ArrayList array3 = new ArrayList();

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

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

          //  array3的forEach参数支持Consumer泛型逆变,

          // Person是Girl的超类型,所以Consumer是Consumer的子类型,

          // 所以,comsumerPerson可以作为forEach的参数。

            array3.forEach(comsumerPerson) 

        }

        通过上面的例子,我们可以看出来:

        • addAll函数中的参数c是作为生产者,从自身读取元素提供给ArrayList。通过协变参数Collection 中读取的元素肯定是ArrayList中元素E的子类型,子类型放进数组中肯定是支持的。

        • forEach函数中的参数action则是作为消费者,从ArrayList拿取元素提供给给自己使用。在array3.forEach(comsumerPerson) 中,Consumer是Consumer的子类型,可以作为参数传入,comsumerPerson拿到array3的Girl元素,在accept函数中统计Person名字含“g”的人员数。这样comsumerPerson的函数功能就得到了很好的复用。

        把只能从中读取的对象称为生产者(Producer),只能写入的对象称为消费者(Consumer),即只能从Producer中get对象,只能put对象给Consumer,这就是著名的PECS原则(Producer-Extends, Consumer-Super),也是协变和逆变的最佳实践原则。

        在狐友的代码中也通过使用协变和逆变,使得通用类可以适配大量的数据类型。以单例的事件总线LivedataBus为例说明:

        Flutter Dart:泛型的协变与逆变_第2张图片

        事件总线bus通过BusMutableLiveData声明支持协变,这样就可以在总线bus中添加所有BusEvent子类型事件。比如BusMutableLiveData (TeamUpPublishEvent 继承BusEvent),BusMutableLiveData

        事件总线bus通过Event的类型名称作为key进行事件发布和订阅的匹配桥梁,让发布的事件可以通知到所有订阅者。

        Flutter Dart:泛型的协变与逆变_第3张图片

        Flutter Dart:泛型的协变与逆变_第4张图片

        BusMutableLiveData的observe方法,通过Observer声明参数支持逆变,让在onChange处理相似数据类型的Observer,可以有机会被多次复用。

        而BusMutableLiveData作为可感知生命周期,可观察数据变化的类型,用它来包装总线事件BusEvent,可以方便业务订阅各种事件通知, 如订阅组局发布事件 TeamUpPublishEvent,组局搜索事TeamUpSearchResultEvent :

        Flutter Dart:泛型的协变与逆变_第5张图片

        a2a831043d6b371648c78c75d75e120e.jpeg

        这里,TeamUpPublishEvent的事件订阅者,会在onChanged收到事件时,先判断发布成功才会刷新UI。TeamUpSearchResultEvent的事件订阅者,则会在onChanged收到事件时上报搜索的结果。

        所以,通过运用协变和逆变,事件总线LivedataBus只用了简单少量的代码,就支持了无数种事件类型的发布和订阅处理。

        三、Dart协变

        实际上在Dart1.x的版本种是既支持协变又支持逆变,但是在Dart2.x版本开始仅支持协变。所以后面我们就不再讨论Dart的逆变。

        在Dart中所有泛型类都默认支持协变(类似Java的<?extend T>,Kotlin的),不需要像Java或Kotlin一样,需要用额外的关键字声明。

        从上一节的分析可知,协变实际上就是泛型结合体保留了泛型参数的子类型化关系比如说int是num的子类型,所以List就是List的子类型,这就是结合体的子类型关系保留了泛型参数的子类型关系,用例子说明:

        // 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 persons) { //根据多态的概念,persons可以传入List or 其子类型的对象。

          for (var person in persons) {

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

          }

        }

        void main() {

        List girls = [];

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

          // Girl是Person的子类型,List支持协变,则List是List子类型。

          // 而PrintMsg函数接收一个List类型,根据协变的原则,可以使用子类型代替超类型,可以使用List类型替代。

          PrintMsg(girls);

          List boys = [];

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

          PrintMsg(boys);// 同上,List也是List的子类型

          List persons = [];

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

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

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

        }

        从上面例子可以看出,在Dart中泛型声明默认是支持协变的,不需要像Java那样在代码中额外声明。

        我们在Flutter项目中,也有大量的泛型协变的应用,以实际代码举例:

        Flutter Dart:泛型的协变与逆变_第6张图片

        在buildBody方法中,接受一个List参数进行Page构建。

        再看调用的位置,实际上传参是一个通过buildButton返回的ElevatedButton(继承自Widget)构建出来的一个List,如下所示:

        Flutter Dart:泛型的协变与逆变_第7张图片

        因为List默认支持协变,所以buildBody方法可以接受一个List类型的实参。buildBody方法正是通过协变,支持了更多种类的页面数据,从而构建起item不同,布局类似的page页面。

        四、Dart协变安全

        Java,Kotlin中协变和逆变都是安全的,但是Dart的泛型型变是存在安全问题的,因为Dart中的协变是支持读写的,而Java,Kotlin中协变是不支持写的。

        以List集合为例,它在Java和Kotlin中都是不变的。

        Java通过声明List变量支持协变,并限制其数据T只能读不能写,从而保证了泛型协变安全

        Kotlin和Java一样,也通过声明List来支持协变并限制其数据只读。但Kotlin对集合做了进一步优化,通过把集合分为可读写集合MutableList和只读集合List来保证安全问题。其中MutableList是可读可写,但不支持协变和逆变,而List只读,且支持协变。

        Dart中,List默认支持协变,又是可读可写的,这样就会存在安全问题,我们举例说明:

        // 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 persons) { //根据多态的概念,persons可以传入List or 其子类型的对象。但是,这里的List实际是不安全的!!!

          for (var person in persons) {

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

          }

        }

        void main() {

        List girls = [];

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

        PrintMsg(girls);  // Girl是Person的子类型,List支持协变,则List是List子类型。

        }

        上面代码编译和运行都没有问题,但是PrintMsg中的List参数实际是不安全的!!!我们在上面的PrintMsg中对参数进行写操作,如下:

        // Dart

        void PrintMsg(List persons) { 

           //根据多态的概念,persons可以传入List or 其子类型的对象。但是,这里的List类型,因为可写,实际上是不安全的!!!

          // 在Dart1.x版本中运行也是可以通过的,原因不做探究。但是后续main通过girls读出一个Boy来也会出错!

          // 在Dart2.x版本中运行是会报错的:type 'Boy' is not a subtype of type 'Girl'

          // 在Dart中,List都是可读写的,协变的。看似在List中添加Boy,实际上是在List中添加Boy。如果后续从List中读出一个Boy来,这是不允许的。

          persons.add(Boy("b999")); // 编译通过,但运行报错!!!

          for (var person in persons) {

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

          }

        }

        main() {

        List girls = [];

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

        PrintMsg(girls);  // Girl是Person的子类型,List支持协变,则List是List子类型。

        }

        而在Kotlin中的不会存在上面那种问题。Kotlin中集合分为可读写集合MutableList和只读集合List, 举例说明:

        // Kotlin

        fun PrintMsg(persons: List) { 

            // Kotlin中List支持协变,且只读。这样的泛型类型是具有安全性的。

            //因为在Kotlin中List被定义为只读集合List

            //没有add, remove等写操作方法,

            //所以List中是无法添加一个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是List子类型,

           // 可以作为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子类代码为例说明:

        Flutter Dart:泛型的协变与逆变_第8张图片

        Flutter Dart:泛型的协变与逆变_第9张图片

        父类State中didUpdateWidget方法只能知道要通知StatefullWidget的更新,通过convariant关键字,可以在子类_KKPageViewState的didUpdateWidget方法中,限制其参数必须是KKPageView (KKPageView 是 StatefullWidget的子类,支持参数协变)。

        六、总结

        通过上面的学习,我们对Dart中的协变有了更深入的理解,开发过程中也能更好更安全地运用Dart中的协变。我们最后总结下文章的核心内容:

        1. 泛型是为了功能复用,减少模板代码而提出来的一种设计思想。

        2.子类关系是一种继承关系,子类型关系则是一种多态关系。子类关系一般是子类型关系,但子类型关系不一定是子类关系。

        3.泛型的逆变,协变和不变,描述的是类型转换关系,是为了泛型结合体支持多态而提出来的。PECS是其最佳的实践原则。

        4.Dart的新版本不支持逆变,支持协变,且默认支持协变。但是Dart的协变支持写操作,有安全隐患,使用时需要额外注意。

        你可能感兴趣的:(flutter,android,kotlin,开发语言,java)