【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使用

这意味着:

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

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

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

    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之间的继承关系,参考下一小节

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

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

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

看如下的实例:

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的继承关系

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,一般会最先执行

线性化的覆盖实例

参考如下代码

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 关键字产生类似于 继承 的关系

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的执行顺序

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 混合结构如下:

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)

【Flutter】一文读懂混入类Mixin的更多相关文章

  1. 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现

    一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...

  2. 即时通讯新手入门:一文读懂什么是Nginx?它能否实现IM的负载均衡?

    本文引用了“蔷薇Nina”的“Nginx 相关介绍(Nginx是什么?能干嘛?)”一文部分内容,感谢作者的无私分享. 1.引言   Nginx(及其衍生产品)是目前被大量使用的服务端反向代理和负载均衡 ...

  3. 大数据篇:一文读懂@数据仓库(PPT文字版)

    大数据篇:一文读懂@数据仓库 1 网络词汇总结 1.1 数据中台 数据中台是聚合和治理跨域数据,将数据抽象封装成服务,提供给前台以业务价值的逻辑概念. 数据中台是一套可持续"让企业的数据用起 ...

  4. 一文读懂MySQL的事务隔离级别及MVCC机制

    回顾前文: 一文学会MySQL的explain工具 一文读懂MySQL的索引结构及查询优化 (同时再次强调,这几篇关于MySQL的探究都是基于5.7版本,相关总结与结论不一定适用于其他版本) 就软件开 ...

  5. 一文读懂Java动态代理

    作者 :潘潘 日期 :2020-11-22 事实上,对于很多Java编程人员来说,可能只需要达到从入门到上手的编程水准,就能很好的完成大部分研发工作.除非自己强主动获取,或者工作倒逼你学习,否则我们好 ...

  6. Java8 函数式【1】:一文读懂逆变

    Java8 函数式[1]:一文读懂逆变 禁止转载 pure function 协变 逆变 Java8 引入了函数式接口,从此方法传参可以传递函数了,有人说: 不就是传一个方法吗,语法糖! lambda ...

  7. 一文读懂HTTP/2及HTTP/3特性

    摘要: 学习 HTTP/2 与 HTTP/3. 前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何 ...

  8. 一文读懂高性能网络编程中的I/O模型

    1.前言 随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力.本文(和下篇<高性能网络编程(六):一文读懂高性能网络编程中的线程模型>)旨在为大家提供有用的 ...

  9. 从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路

    本文原作者阮一峰,作者博客:ruanyifeng.com. 1.引言 HTTP 协议是最重要的互联网基础协议之一,它从最初的仅为浏览网页的目的进化到现在,已经是短连接通信的事实工业标准,最新版本 HT ...

  10. 一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm)

    一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm) 2017-12-25  16:29:19   对于 A3C 算法感觉自己总是一知半解,现将其梳理一下,记录在此,也 ...

随机推荐

  1. 学习LXC(Linux 容器)技术

    安装LXC.LXD.zfs 测试机器为ubuntu sudo apt-get install lxc lxd zfsutils-linux -y 创建LXD的zfs存储池 sudo lxd init ...

  2. C/C++八大排序

    排序 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存. 按照难易程度排序,八大排序算法可以从简单到复杂 ...

  3. ENVI、ERDAS计算Landsat 7地表温度:单窗算法实现

    本文介绍基于ENVI与ERDAS软件,对Landsat 7遥感影像数据加以单窗算法的地表温度(LST)反演操作. 目录 1 原理部分与前期操作准备 1.1 图像预处理 1.2 植被指数反演 1.3 单 ...

  4. 在Java中List, Set, Map是否继承自Collection接口?

    List和Set是继承自Collection接口的接口,Set不允许重复的项目,List允许重复项目, Set接口派生的类有TreeSet,HashSet,LinkedHashSet. List接口派 ...

  5. css使用背景灵活展示雪碧图

    雪碧图是把各种小图标集合在一起的png图片,通过background-position来展示雪碧图中不同位置的小图标,比如以下图片,在项目中要用到的小图标很多,如果每一个图标都作为一个png或者jpg ...

  6. __wakeup()魔术方法绕过(CVE-2016-7124)

    __wakeup()魔术方法绕过(CVE-2016-7124) 漏洞简介 在php反序列化数据过程中,如果类中存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方 ...

  7. Amiya 前端UI

    最近在使用一个基于Ant Design 二次封装的组件 Git文档地址 Index - Amiya (gitee.io)

  8. 如何做一个api接口?

    程序员是公司里的技术岗位,是产品经理最亲密的伙伴.但是程序员可以理解产品经理的工作,产品经理却不一定理解程序员的工作,所以经常被无良程序员欺骗.从API接口这个维度,分析API的概念以及为什么要了解它 ...

  9. 吃透单调栈(2)——解两道Hard题:接雨水、柱状图中最大的矩形问题

    怎么想到要用单调栈的? 这类题目的数据通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置(寻找边界),此时我们就要想到可以用单调栈了. 42. 接雨水 这道题就是要求解每一 ...

  10. LeetCode买卖股票之一:基本套路(122)

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于<LeetCode买卖股票>系列 在L ...