Flutter 语法进阶 | 深入理解混入类 mixin


theme: cyanosis

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天,点击查看活动详情


混入类引言

混入类是 Dart 中独有的概念,它是 继承实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类接口 的中间地带。下面就来认识一下混入类的 使用与特性


1. 混入类的定义与使用

混入类通过 mixin 关键字进行声明,如下的 MoveAble 类,其中可以持有 成员变量 ,也可以声明和实现成员方法。对混入类通过 with 关键字进行使用,如下的 Shape 混入了 MoveAble 类。在下面 main 方法测试中可以看出,混入一个类,就可以访问其中的成员属性与方法,这点和 继承 非常像。

dart void main(){  Shape shape = Shape();  shape.speed = 20;  shape.move();//=====Shape move====  print(shape is MoveAble);// true } ​ mixin MoveAble{  double speed = 10;  void move(){    print("=====$runtimeType move===="); } } ​ class Shape with MoveAble{ ​ }


一个类可以混入若干个类,通过 , 号隔开。如下 Shape 混入了 MoveAblePaintAble ,就表示 Shape 对象可以同时拥有这两个类的能力。这个功能和接口有点相似,不过混入类可以进行对方法进行实现,这点要比接口更灵活。有点 随插随用 的感觉,甚至 Shape 类中可以什么都不做,就坐拥 “王权富贵”

dart mixin PaintAble{  void paint(){    print("=====$runtimeType paint===="); } } ​ class Shape with MoveAble,PaintAble{ }

值得注意一点的是:混入类支持 抽象方法 ,而且同样要求派生类必须实现 抽象方法 。如下 PaintAbletag1 处定义了 init 抽象方法,在 Shape 中必须实现,这一点又和 抽象类 有些相像。所以我说混入类像是 抽象类接口 的中间地带,它不像继承那样单一,也不像接口那么死板。

dart mixin PaintAble{  late Paint painter;  void paint(){    print("=====$runtimeType paint===="); }  void init();// tag1 } ​ class Shape with MoveAble,PaintAble{  @override  void init() {    painter = Paint(); } }


2. 混入类对二义性的解决方式

通过前面可以看出,混入类 可谓 上得厅堂下得厨房 ,能打能抗的六边形战士。但,没有任何事物是完美无缺的。仔细想一下,既然语法上支持 多混入 ,那解决二义性就是一座不可避免大山。接口 牺牲了 普通成员方法实现 ,可谓断尾求生,才解决二义性问题,支持 多实现 。而 混入类 又能写成员变量,又能写成员方法,那它牺牲了什么呢?答案是:

混入类不能拥有【构造方法】

这一点就从本质上限制了 混入类 无法直接创建对象,这也是它和 普通类 最大的差异。从这里可以看出,抽象类接口混入类 都具有不能直接实例化的特点。本质上是因为这三者都是更高层的抽象,可能存在未实现的功能。那为什么混入类无法构造,就能解决二义性问题呢?下面来分析一下,两个混入类中的同名成员、同名方法,在多混入场景中是如何工作的。如下测试代码所示,AB 两个混入类拥有同名的 成员属性成员方法 :

dart mixin A {  String name = "A"; ​  void log() {    print(name); } } ​ mixin B {  String name = "B"; ​  void log() {    print(name); } }

此时,C 依次混入 AB 类,然后实例化 C 对象,执行 log 方法,可以看出,打印的是 B

dart class C with A, B {} ​ void main() {  C c = C();  c.log(); // B }

如果 C 依次混入 BA 类,打印结果是 A 。也就是说对于多混入来说,混入类的顺序是至关重要的,当存在二义性问题时,会 “后来居上” ,访问最后混入类的方法或变量。这点往往会被很多人忽略,或压根不知道。

dart class C with B, A {} ​ void main() {  C c = C();  c.log(); // A }


另外,补充一个小细节,如果 C 类覆写了 log 方法,那么执行时毋庸置疑是走 C#log 。由于混入类支持方法实现,所以派生类中可以通过 super 关键字触发 “基类” 的方法。同样对于二义性的处理也是 “后来居上” ,下面的 super.log() 执行的是 B 类方法。这种特性常用于对有生命周期的类进行拓展的场景,比如 AutomaticKeepAliveClientMixin

class C with A, B {    @override  void log() {    super.log();// B    print("C"); } }


3.混入类间的继承细节

另外,两个混入类间可以通过 on 关键字产生类似于 继承 的关系:如下 MoveAble on Position 之后,MoveAble 类中可以访问 Position 中定义的 vec2 成员变量。


但有一点要特别注意,由于 MoveAble on Position ,当 Shape with MoveAble 时,必须在 MoveAble 之前混入 Position 。这点可能很多人也都不知道。

dart class Shape with Position,MoveAble,PaintAble{ ​ }


另外,混入类并非仅由mixin 声明,一切满足 没有构造方法 的类都可以作为混入类。比如下面 A普通类B接口(抽象)类 ,都可以在 with 后作为 混入类被对待 。也就是说,一个类的可以用多重身份,并非是互斥的,它具体是什么身份,要看使用的场景。而使用场景最醒目的标志是 关键字

| 关键字 | 类关系 | 耦合性 | | ---------- | --- | --- | | extend | 继承 | 高 | | implements | 实现 | 低 | | with | 混入 | 中 |

dart class A {  String name = "A"; ​  void log() {    print(name); } } ​ abstract class B{  void log(); } ​ class C with A, B { ​  @override  void log() {    super.log();// B    print("C"); } }


4.根据源码理解混入类

混入类在 Flutter 框架层的使用是非常多的,在 《Flutter 渲染机制 - 聚沙成塔》的 十二章 结合源码介绍了混入类的价值。下面来举个混入类的使用场景,会有些难,新手适当理解。比如 AutomaticKeepAliveClientMixin 继承 State :

dart mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {}

所以它可以在 State 的生命周期相关回调方法中做额外的处理,来实现某些特定的功能能。

这样,当在 State 派生类中混入 AutomaticKeepAliveClientMixin ,根据混入类二义性的特点,对于已经覆写的方法,可以通过 super.XXX 访问混入类的功能。对于未覆写的方法,会默认走混入类方法,这样就可以形成一种 "可插拔" 的功能件。

举个更易懂的例子,如下定义一个 LogStateMixin ,对 initStatedispose 方法进行覆写并输出日志。这样在一个 State 派生类中混入 LogStateMixin 就可以不动声色地实现生命周期打印功能,不想要就不混入。对于一些逻辑相对独立,或可以进行复用的拓展功能,使用 mixin 是非常方便的。

mixin LogStateMixin<T extends StatefulWidget> on State<T> { ​  @override  void initState() {    super.initState();    print("====initState===="); } ​  // 略其他回调...    @override  void dispose() {    super.dispose();    print("====dispose===="); } }

源码中有大量的混入类应用场景,大家可以自己去发现一下。本文从更深层次,分析了混入类的来龙去脉,它和 继承接口 的差异。作为 Dart 中相对独立的概念,对混入类的理解是非常重要的,它相当于在原有的 类间六大关系 中又添加了一种。本文想说的就这么多,谢谢观看~

通过本课程学习您可以学习到Dart语言如下知识:第1章 Dart语言概述:Dart语言简介、Dart语言支持平台开发、Flutter为什么选择Dart语言。第2章 开发环境搭建:下载Dart SDK、Windows下安装Dart SDK、macOS下安装Dart SDK、Visual Studio Code开发工具、IntelliJ IDEA开发工具。第3章 完成个Dart程序:动动手写一个HelloWorld、Dart源代码文件组织结构、Visual Studio Code调试Dart代码、IntelliJ IDEA调试Dart代码。第4章 Dart语法基础:标识符、关键字、变量、常量、注释、库。第5章 Dart数据型:数值型、字符串、数据型相互转换、布尔型和枚举型第6章 运算符:算术运算符、算术赋值运算符、关系运算符、逻辑运算符、位运算符、条件运算符、型检查运算符。第7章 控制语句:分支语句、循环语句、跳转语句。第8章 函数:函数声明、可选参数、头等函数(first-class function)、匿名函数。第9章 声明、getter和setter访问器、构造函数、静态变量和静态方法、级联符号。第10章 继承与多态:Dart中的继承实现、调用父构造函数、成员变量的覆盖(Override)、方法的覆盖(Override)、多态、混入Mixins)。第11章 抽象与接口:抽象、接口、Dart中隐式接口、实现接口。第12章 数据容器:List容器、Set容器、Map容器、泛型。第13章 异常处理:捕获异常、try-on捕获异常、try-catch捕获异常、try-on-catch捕获异常、使用finally代码块、手动抛出异常、自定义异常。第14章 异步编程:Dart异步处理机制、案例:同步函数实现读取文件、案例:异步函数实现读取文件、Future对象。  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张风捷特烈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值