Flutter 语法进阶 | 深入理解混入类 mixin
混入类引言
混入类是 Dart 中独有的概念,它是 继承 、实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类 和 接口 的中间地带。下面就来认识一下混入类的 使用与特性 。
1. 混入类的定义与使用
混入类通过 mixin 关键字进行声明,如下的 MoveAble 类,其中可以持有 成员变量 ,也可以声明和实现成员方法。对混入类通过 with 关键字进行使用,如下的 Shape 混入了 MoveAble 类。在下面 main 方法测试中可以看出,混入一个类,就可以访问其中的成员属性与方法,这点和 继承 非常像。
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 混入了 MoveAble 和 PaintAble ,就表示 Shape 对象可以同时拥有这两个类的能力。这个功能和接口有点相似,不过混入类可以进行对方法进行实现,这点要比接口更灵活。有点 随插随用 的感觉,甚至 Shape 类中可以什么都不做,就坐拥 “王权富贵”。
mixin PaintAble{
void paint(){
print("=====$runtimeType paint====");
}
}
class Shape with MoveAble,PaintAble{
}
值得注意一点的是:混入类支持 抽象方法 ,而且同样要求派生类必须实现 抽象方法 。如下 PaintAble 的 tag1 处定义了 init 抽象方法,在 Shape 中必须实现,这一点又和 抽象类 有些相像。所以我说混入类像是 抽象类 和 接口 的中间地带,它不像继承那样单一,也不像接口那么死板。
mixin PaintAble{
late Paint painter;
void paint(){
print("=====$runtimeType paint====");
}
void init();// tag1
}
class Shape with MoveAble,PaintAble{
@override
void init() {
painter = Paint();
}
}
2. 混入类对二义性的解决方式
通过前面可以看出,混入类 可谓 上得厅堂下得厨房 ,能打能抗的六边形战士。但,没有任何事物是完美无缺的。仔细想一下,既然语法上支持 多混入 ,那解决二义性就是一座不可避免大山。接口 牺牲了 普通成员 和 方法实现 ,可谓断尾求生,才解决二义性问题,支持 多实现 。而 混入类 又能写成员变量,又能写成员方法,那它牺牲了什么呢?答案是:
混入类不能拥有【构造方法】
这一点就从本质上限制了 混入类 无法直接创建对象,这也是它和 普通类 最大的差异。从这里可以看出,抽象类 、接口 、混入类 都具有不能直接实例化的特点。本质上是因为这三者都是更高层的抽象,可能存在未实现的功能。那为什么混入类无法构造,就能解决二义性问题呢?下面来分析一下,两个混入类中的同名成员、同名方法,在多混入场景中是如何工作的。如下测试代码所示,A 、B 两个混入类拥有同名的 成员属性 和 成员方法 :
mixin A {
String name = "A";
void log() {
print(name);
}
}
mixin B {
String name = "B";
void log() {
print(name);
}
}
此时,C 依次混入 A、B 类,然后实例化 C 对象,执行 log 方法,可以看出,打印的是 B 。
class C with A, B {}
void main() {
C c = C();
c.log(); // B
}
如果 C 依次混入 B、A 类,打印结果是 A 。也就是说对于多混入来说,混入类的顺序是至关重要的,当存在二义性问题时,会 “后来居上” ,访问最后混入类的方法或变量。这点往往会被很多人忽略,或压根不知道。
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 。这点可能很多人也都不知道。
class Shape with Position,MoveAble,PaintAble{
}
另外,混入类并非仅由mixin 声明,一切满足 没有构造方法 的类都可以作为混入类。比如下面 A 是 普通类 ,B 是 接口(抽象)类 ,都可以在 with 后作为 混入类被对待 。也就是说,一个类的可以用多重身份,并非是互斥的,它具体是什么身份,要看使用的场景。而使用场景最醒目的标志是 关键字 :
| 关键字 | 类关系 | 耦合性 |
|---|---|---|
| extend | 继承 | 高 |
| implements | 实现 | 低 |
| with | 混入 | 中 |
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 :
mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {}
所以它可以在 State 的生命周期相关回调方法中做额外的处理,来实现某些特定的功能能。
这样,当在 State 派生类中混入 AutomaticKeepAliveClientMixin ,根据混入类二义性的特点,对于已经覆写的方法,可以通过 super.XXX 访问混入类的功能。对于未覆写的方法,会默认走混入类方法,这样就可以形成一种 "可插拔" 的功能件。
举个更易懂的例子,如下定义一个 LogStateMixin ,对 initState 和 dispose 方法进行覆写并输出日志。这样在一个 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 中相对独立的概念,对混入类的理解是非常重要的,它相当于在原有的 类间六大关系 中又添加了一种。本文想说的就这么多,谢谢观看~
作者:张风捷特烈
链接:https://juejin.cn/post/7132651702980706312
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。