【Flutter】一文读懂混入类Mixin

【Flutter】一文读懂混入类Mixin

基本介绍

Mixin是一种有利于代码复用,又避免了多继承的解决方案。

Mixin 是面向对象程序设计语言中的类,提供了方法的实现,其他类可以访问 Mixin 类的方法而不必成为其子类;Mixin 为使用它的 Class 类提供额外的功能,但自身却不单独使用(不能单独生成实例对象,属于抽象类),Mixin 类通常作为功能模块使用,在需要该功能时"混入",而且不会使类的关系变得复杂;
Mixin 有利于代码复用性同时又避免了多继承的复杂性,使用 Mixin 享有单一继承的单纯性和多重继承的共有性,interface 接口与 Mixin 相同的地方是都可以多继承,不同的地方在于 Mixin 是可以实现的;

对应关系

继承 混入 接口
关键字 extends with implements
对应数量 1:1 1:n 1:n
代码设置顺序
耦合度

举例学习

首先,众所周知...Java只能单继承,

假如我们面临下面这一种需求:

,我们需要用多个对象表示一些 动物, 诸如 狗、鸟、鱼、青蛙。其中

  1. 狗会跑
  2. 鸟会飞
  3. 鱼会游泳
  4. 青蛙是两栖动物,会跑,并且会游泳

基于如下一些考虑

  • 动物特性可能会继续增多,并且一个动物可能具备多种技能
  • 动物种类很多,但是可以归大类。例如 鸟禽、哺乳类

我们使用如下设计

  • 动物继承自 Animal 抽象类
  • 跑、飞、游 抽象为接口

我们按照上面的需求...让copilotX帮我写一个类的实现...

可以看到AI生成的代码还是很给力的,但是我们可以发现,Frog和Dog都实现了Run的抽象方法。

假如我们现在尝试让代码复用率变高,让Run,Fly,Swim作为实现,看看会发生什么...

可以看到,我们的Copilit告诉了我们问题

原来这个写法 Dart 会一直认为 super 调用是在调用一个 abstract 的函数,所以我们这时候需要把这里面集成的函数实现一一实现。

这时候问题来了,Frog 和 Fish 都实现了 Swim 接口,这时候 swim 函数的内容我们需要重复的写 2 遍!

(当然我们指的就是前面AI生成的代码)

当然,作为一篇Mixin教学,我们对这个结果肯定是不满意的...

现在,我们完全没学过类似Java的default关键字的知识点...我们只是个渴望dart的小白...

选择使用mixin,重新定义Run,Fly,Swim方法,子类也不再是实现接口而是混入。

可以看到,mixin被混入到了类中,也实现了对应"抽象类"的特性。

这里类的继承关系我们可以梳理成下图

这里也可以增加一个新的理解:mixin并不是对子类的拓展,而是对父类的拓展

mixin,class,interface的异同

mixin也可以使用class关键字定义,也可以当做普通class一样使用。

mixin可以使用with定义,这样定义的mixin就只能通过with关键字引用了。

Dart是没有interface这种东西的,但并不意味着这门语言没有接口,事实上,Dart任何一个类都是接口,你可以实现任何一个类,只需要重写那个类里面的所有具体方法。

所以,Dart中的任何一个class,既是类,又是接口,也可以当作mixin使用

这意味着:

  • 混入类可以持有成员变量,也可以声明和实现成员方法。而混入一个类,就可以访问其中的成员属性和方法,这点和继承很像

  • 一个类可以混入若干个类,通过,分隔开,这个功能和接口类似,但是和接口不同的是:混入类本身可以对方法进行实现,而接口内必须是抽象方法

  • 混入类支持抽象方法 ,但是这要求了派生类必须实现抽象方法,这一点又和抽象类很像。

    dart 复制代码
    mixin PaintAble{
      late Paint painter;
      void paint(){
        print("=====$runtimeType paint====");
      }
      void init();
    }
    
    class Shape with MoveAble,PaintAble{
      @override
      void init() {
        painter = Paint();
      }
    }
    // 这里的Shape作为派生类,必须实现PaintAble中声明的抽象方法init

mixin的限制

可以看到,在混入了之后,就可以使用mixin的所有方法了,但是有时我们并不希望所有类都可以使用一些方法。比如我在Dog类中with一个Fly,这就意味着我们的狗可以飞了!

所以...为了守护自然界的秩序,mixin提供了一种限制:on 关键字

规定了:on后面衔接的类和它的子类才可以被混入

除此之外,on还可以限定mixin之间的继承关系,参考下一小节

dart 复制代码
mixin Fly on Bird{
    void fly(){
        print('只有鸟类可以混入Fly')
    }
}

除了类的限制外,mixin本身就是一种限制。

因为刚刚提到,dart中的任何一个类都可以被混入,而使用mixin声明的类,需要使用with关键字才可以替换。

除此之外的一点小改动...

细心的你可能会发现,在我们的样例中直接这样修改是没办法通过编译的。这是因为上面那句话:

mixin并不是对子类的拓展,而是对父类的拓展,也就是说,我们在代码中,相当于将Animal拓展了一个Fly功能,而我们规定了,Fly方法只能被Bird及Bird的子类使用。Animal并不属于Bird的子类(反倒是他的父类),所以会报错。

继承的二义性问题

先说说什么是二义性问题:

(内容参考如下文章:C++多继承中的二义性问题_继承的二义性-CSDN博客

在C++中,派生类继承基类,对基类成员的访问应该是确定的、唯一的,但是常常会有以下情况导致访问不一致,产生二义性。

1.在继承时,基类之间、或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性------同名二义性。

2.当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生另一种不确定性------路径二义性。

而在接口中,牺牲了接口的普通成员方法实现,最终才解决二义性问题,最终能够支持多实现。

混入类中,不能拥有构造方法 ,也就是说不能实例化 。这一点跟抽象类接口是一样的。

看如下的实例:

dart 复制代码
class S {
  fun() => print('A');
}

mixin MA {
  fun() => print('MA');
}
mixin MB {
  fun() => print('MB');
}

class A extends S with MA, MB {}

class B extends S with MB, MA {}

main() {
  A a = A();
  a.fun();
  B b = B();
  b.fun();
}

运行代码,得到如下的结果:

MB

MA

我们可以得出结论:最后一个混入的mixin,会覆盖前面的mixin的特性

为了验证这个结论,我们给mixin加入super调用和mixin的继承关系

dart 复制代码
mixin MA on S {
  fun() {
    super.fun();
    print('MA');
  }
}
mixin MB on S {
  fun() {
    super.fun();
    print('MB');
  }
}

运行代码,得到如下结果:

A

MA

MB

A

MB

MA

这里我们得到mixin的工作方式:线性化

Mixin的线性化

Dart 中的 mixin 通过创建一个类来实现,该类将 mixin 的实现层叠在一个超类之上以创建一个新类 ,它不是"在超类中",而是在超类的"顶部"。

我们可以得到以下几个结论:

  1. mixin 可以实现类似多重继承的功能,但是实际上和多重继承又不一样。多重继承中相同的函数执行并不会存在 "父子" 关系
  2. mixin 可以抽象和重用一系列特性
  3. mixin 实际上实现了一条继承链
  4. A is S,A is MA,A is MB。

最终我们可以得出一个很重要的结论

声明 mixin 的顺序代表了继承链的继承顺序,声明在后面的 mixin,一般会最先执行

线性化的覆盖实例

参考如下代码

dart 复制代码
class S {
  fun()=>print('A');
}
mixin MA on S {
  fun() {
    super.fun();
    log();
    print('MA');
  }

  log() {
    print('log MA');
  }
}
mixin MB on S {
  fun() {
    super.fun();
    print('MB');
  }

  log() {
    print('log MB');
  }
}

class A extends S with MA,MB {}
A a = A();
a.fun();

按照我们常见的思维方式,可能会认为得到的结论为:

A

log MA

MA

MB

但事实上,得到的输出结果为:

A

log MB

MA

MB

因为按照上面的工作原理,在mixin的继承链建立了之后,最后声明的mixin会把前面声明的mixin函数覆盖掉,所以即使我们此时在MA函数中调用了log,而事实上MA里面的log函数被MB覆盖了,最后调用的是MB。

小结论:调用了super就可以从前往后看执行顺序,如果存在函数内同名调用函数的情况要从后往前看

混入类之间的继承关系

另外,两个混入类间可以通过 on 关键字产生类似于 继承 的关系

dart 复制代码
mixin A{
    int i = 5;
}
mixin B on A{
    int j = 6;
    void show(){
        print(i);
        print(j);
    }
}
class C with A,B{

}
main(){
    C c = new C();
    c.show();
}

可以看到,B中可以通过on A来访问A内的成员变量。

同时C with A,B不可以调换顺序,否则编译器会报错。这也符合我们之前说的线性关系,因为"B继承A",所以,只有"B覆盖了A"这种线性关系才是可以被接受的。

extends,mixin,implements的执行顺序

dart 复制代码
class Ex{
  Ex(){
    print('extends constructor');
  }
  void show(){
    print('extends show');
  }
}

// dart 没有 interface 关键字,但是可以使用 abstract class 来实现接口的功能
abstract class It{
  void show();
}


mixin mx1 on Ex{
  void show(){
    super.show();
    print('mx1show');
  }
}

mixin mx2 on Ex{
  void show(){
    super.show();
    print('mx2show');
  }
}

class C12 extends Ex with mx1,mx2 implements It{
  @override
  void show() {
    super.show();
    print('it show');
  }
}

class C21 extends Ex with mx2,mx1 implements It{
  @override
  void show() {
    super.show();
    print('it show');
  }
}

void main(){
  C12 c12 = new C12();
  c12.show();
  C21 c21 = new C21();
  c21.show();
}

执行结果:

extends constructor

extends show

mx1show

mx2show

it show

extends constructor

extends show

mx2show

mx1show

it show

结论:执行顺序是 extends 继承优先执行,之后是 with 混入,最后是 implements 接口重载;

Flutter的runAPP

接下来我们回到Flutter,看一下runAPP()的形式

WidgetsFlutterBinding.ensureInitialized 方法如下:

WidgetsFlutterBinding 混合结构如下:

dart 复制代码
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

BindingBase 及构造函数如下:

其执行了 initInstances 和 initServiceExtensions 方法。看下面混合的顺序:

从后到前依次执行其 initInstances 和 initServiceExtensions(如果有) 方法,由于 initInstances 和 initServiceExtensions 方法中首先执行 super.initInstances()super.initServiceExtensions() ,所以最后执行的顺序为:BindingBase -> GestureBinding -> SchedulerBinding -> ServicesBinding -> PaintingBinding -> SemanticsBinding -> RendererBindinsg -> WidgetsBinding 。

而在WidgetsBinding和RendererBinding中,都有一个叫做drawFrame的函数,而Widget的drawFrame调用了super.drawFrame,同时Widgets on Renderer

这里反应的逻辑有如下两点:

  • 保证widget等的drawFrame能够先于render调用。保证了flutter在布局和渲染处理时 widgets->render
  • 保证了顺序的同时,两者仍然各个负责自己的部分

参考文章

Flutter 语法进阶 | 深入理解混入类 mixin - 掘金 (juejin.cn)

彻底理解 Dart mixin 机制 - 掘金 (juejin.cn)

Flutter 必知必会系列 ------ mixin 和 BindingBase 的巧妙配合 - 掘金 (juejin.cn)

【Flutter 专题】103 初识 Flutter Mixin - 掘金 (juejin.cn)

跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin - 掘金 (juejin.cn)

Flutter 中不得不会的 mixin - 老孟Flutter - 博客园 (cnblogs.com)

深入理解 Dart mixin 机制 - 知乎 (zhihu.com)

C++多继承中的二义性问题_继承的二义性-CSDN博客

Flutter 必知必会系列 ------ runApp 做了啥 - 掘金 (juejin.cn)