【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. K8S 对象

    本页说明了在 Kubernetes API 中是如何表示 Kubernetes 对象的, 以及使用 .yaml 格式的文件表示 Kubernetes 对象. https://kubernetes.io ...

  2. 微服务性能分析工具 Pyroscope 初体验

    Go 自带接口性能分析工具 pprof,较为常用的有以下 4 种分析: CPU Profiling: CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主 ...

  3. SQL: Unknown collation: 'utf8mb4_0900_ai_ci'

    错误原因 字符集错误,我的版本是5.7,文件中是8.0 解决方案 替换字符集 utf8mb4_0900_ai_ci替换为utf8_general_ci utf8mb4替换为utf8 注意:注释中的部分 ...

  4. protoc-gen-doc 自定义模板规则详解

    protoc-gen-doc 自定义模板规则详解 配套演示工程 此项目中所用 proto 文件位于 ./proto 目录下,来源于 官方proto示例 此项目中所列所有模板case文件位于 ./tmp ...

  5. Hi3798MV200 恩兔N2 NS-1 (二): HiNAS海纳思使用和修改

    目录 Hi3798MV200 恩兔N2 NS-1 (一): 设备介绍和刷机说明 Hi3798MV200 恩兔N2 NS-1 (二): HiNAS海纳思使用和修改 Hi3798MV200 恩兔N2 NS ...

  6. Iphone常用工具

    iFunBox itools 百度助手 崩溃日志的路径 /var/mobile/Library/Logs/CrashReporter

  7. hihocoder offer收割赛。。#1284

    好久没刷题,水一水,反正排不上名次..这道题记下 我想着蛋疼的做了质因数分解,然后再算的因子个数..慢的一比,结果导致超时,还不如直接一个for循环搞定..也是醉了 最后其实就是算两个数的gcd,然后 ...

  8. WEB组态编辑器插件(BY组态)介绍

    BY组态是一款非常优秀的纯前端的[web组态插件工具],采用标准HTML5技术,基于B/S架构进行开发,支持WEB端呈现,支持在浏览器端完成便捷的人机交互,简单的拖拽即可完成可视化页面的设计.可无缝嵌 ...

  9. C语言指针函数和函数指针区别(转)

    C语言函数指针和指针函数的区别C和C++中经常会用到指针,和数据项一样,函数也是有地址的,函数的地址是存储其机器语言代码的内存的开始地址. 指针函数和函数指针经常会混淆,一个是返回指针的函数,另一个是 ...

  10. 基于三菱Q系列cc-Link的卧式自动燃煤蒸汽锅炉控制系统

    系统说明: 方案选用: 本系统最终采用三菱Q系列+FX3U系列方案 工艺流程: 触摸屏设计: 程序设计: 本文章为原创作品,未经允许,请勿转载,否则将会追究法律责任.