mixin 解决了继承带来的基本问题。它以一种优雅的方式来重用来自不同类的代码,这些类不太适合同一个类层次结构。
mixin 是一个类,其方法和属性可以被其他类使用——无需子类化。
这是一段可重用的代码,可以“插入”到任何需要此功能的类中。 Dart 支持 Mixin,Flutter 在很多地方都使用了 Mixin。作为一名 Flutter 开发人员,可能对在 Flutter 中创建动画时需要使用的 SingleTickerProviderStateMixin 很熟悉。
纯子类化
显然,他们都是 Bird 。它们都具有相同的能力:它们可以 flutter 和 chirp 。因此,让三只鸟都继承自一个共同的超类 Bird 是合理的。只有 QuickBird 有点特别:它还可以 write 很棒的博客文章。在图表中,我们的类层次结构如下所示:
经过一段时间的飞行,三只鸟结交了一个新朋友:Mosquito。Mosquito 也是扇动翅膀的高手。 Dash、QuickBird 和 Parrot 想要欢迎他加入他们的 Bird 小组。但事实证明,他毕竟不是鸟! 他连 chirp 都不会!因此,他不能真正加入的 Bird 类,所以他们决定创建一个额外的超类 FlyingAnimal。让我们将这个新的超类和 Mosquito 添加到我们的类图中:
我们还创建了一个子类 Insect,因为我们的飞行动物群可能会遇到像 Mosquito 一样的其他动物。 FlyingAnimal 类提供了常见的 flutter 能力。 Bird 子类增加了 chirp 的能力。有了这个新的阶级层次,四只动物继续他们的旅程。
他们遇到了一个引起 QuickBird 注意的 Human:他绝对不是飞行动物,因为他甚至没有翅膀。但他正在热情地敲击笔记本电脑的键盘写博客文章,所以他似乎能够 write ,就像我们的 QuickBird 一样。让我们在类图中包含 Human :
现在我们遇到了一个问题:在代码中实现这一点时,我们希望在所有提供相应功能的类中重用函数 flutter()、chirp() 和 write()。在面向对象的语言中,代码重用是通过子类化完成的。但是我们不能为 Bird、Insect 和 Human 创建一个通用的超类来提供其子类所需的所有方法。
- 如果我们将 write() 函数添加到一个通用的超类 Creature 中,所有的 Bird 和 Insect 都会继承这种能力。但只有 QuickBird 和 Human 才应该有这个能力!
- 如果我们创建一个新的超类 BlogWriter 并且只有 QuickBird 和 Human 继承自它,QuickBird 将失去其 Bird flutter 和 chirp 的能力。
- 如果我们根本不创建公共超类,我们将不得不在 QuickBird 和 Human 这两个类中实现 write() 函数,从而导致代码重复,从而违反 DRY 原则(不要重复自己) .
你可能已经注意到,该结构也变得越来越复杂,使得将来更难理解和维护。为了解决这个问题,我们需要一种在多个类层次结构中重用代码的方法。首先想到的是允许类从多个超类继承,但是多重继承会带来菱形问题,并且在 Dart 中不支持。相反,我们使用 mixins!
使用 Dart mixins,我们可以在多个类层次结构中不受任何限制地重用我们的代码。让我们尝试用 mixins 重构我们的类图!首先,我们创建一个 FlutterMixin 来提供 Flutter 的能力。我们将它注入到我们的 Bird 和 Mosquito 类中。接下来,我们定义一个仅注入到 Bird 类的 ChirpMixin 和一个注入到 QuickBird 和 Human 类的 WriteMixin。 (我们只展示第一个 mixin 的代码来说明语法。)
Mixin 可以使用 mixin 关键字或通过普通的类声明来定义。
mixin FlutterMixin on Creature {
int height = 0;
void flutter() {
height++;
}
}
正如你在这个例子中看到的,方法和属性都可以在 mixin 中定义。 on 关键字描述了可以在哪个继承结构上使用 mixin。在这种情况下,FlutterMixin 只能用于 Creatures。不必将 mixin 限制在某个类中,但防止未来的自己或其他开发人员将功能混合到绝对没有业务处理此功能的类中通常很有用。 (例如,编辑和发布博客文章的 mixin 应该限制在 BlogAuthor 类中,这样就不会有人意外地将 EditMixin 添加到 BlogReader 中。)
要将 mixins 的功能添加到 Bird 和 QuickBird 类,我们使用 with 关键字:
class Bird extends Creature with FlutterMixin, ChirpMixin {}
class QuickBird extends Bird with WriteMixin {}
我们可以使用在 mixins 中定义的函数了。例子:
void main(String[] args) {
final quickBird = QuickBird();
quickBird.flutter(); // inherited from Bird which includes the FlutterMixin
quickBird.chirp(); // inherited from Bird which includes the ChirpMixin
quickBird.write(); // directly mixed-in via WriteMixin
}
Mixins 如何适应继承层次结构
让我们花点时间回顾一下我们是如何使用 mixin 修改继承结构的:
既然我们已经将 QuickBird 类与这么多 mixin 混合在一起,那么它的实际类型是什么? mixin 是如何插入到继承层次结构中的?让我们在代码中检查一下!
void main(String[] args) {
final quickBird = QuickBird();
if(quickBird is Creature)
print('I am a Creature'); // prints: "I am a Creature"
if(quickBird is Bird)
print('I am a Bird'); // prints: "I am a Bird"
if(quickBird is FlutterMixin)
print('I am a FlutterMixin'); // prints: "I am a FlutterMixin"
}
正如我们从输出中看到的,mixin 是多态的,就像类一样。一个类假定它的祖先类的类型以及它注入的 mixin 的类型。如果我们看一下运行时的继承结构,我们就会明白为什么它会这样工作:
每个 mixin 在运行时都会创建一个新的类/接口,该类/接口由层次结构中的下一个类继承。 mixins 的顺序很重要,因为它指定了 mixins 和类相互继承的顺序。作为核心规则,继承层次在类的定义中总是从右到左读取:
由于 mixin 在运行时被集成到类层次结构中,它们也可以覆盖方法。这是一个例子:
abstract class Bird {
void move() {
print('Bird ▶︎ The bird is moving');
}
}
mixin FlutterMixin on Bird {
@override
void move() {
super.move();
print('FlutterMixin ▶︎ The bird is fluttering through the air');
}
}
mixin SailMixin on Bird {
@override
void move() {
super.move();
print('SailMixin ▶︎ The bird is sailing in the wind');
}
}
如果我们现在将 QuickBird 定义为 Bird 的子类,并按以下顺序添加两个 mixin
class QuickBird extends Bird with FlutterMixin, SailMixin {}
QuickBird().move();
将调用层次结构中所有超类的 move() 函数并打印以下输出:
Bird ▶︎ The bird is moving
FlutterMixin ▶︎ The bird is fluttering through the air
SailMixin ▶︎ The bird is sailing in the wind
当我们调用 move 函数时,首先调用 SailMixin 的实现,因为它是右边的第一个 mixin。因为它首先调用 super(),所以在打印输出之前调用右边的下一个 mixin(FlutterMixin)的 move() 实现。再次,它首先调用 super() 来执行 Bird 上的 move() 方法。所以首先打印 Bird 的消息,然后是 FlutterMixin 的消息,最后是 SailMixin 的消息。
如果我们改变 FlutterMixin 和 SailMixin 的顺序,
class QuickBird extends Bird with SailMixin, FlutterMixin {}
Bird ▶︎ The bird is moving
SailMixin ▶︎ The bird is sailing in the wind
FlutterMixin ▶︎ The bird is fluttering through the sky
由于 FlutterMixin 现在是右起第一个 mixin,它是类层次结构中最低的,它的 move() 方法首先执行。 super() 调用执行 SailMixin 的 move() 方法,最终执行 Bird 的 move() 方法。所以 Bird 的消息仍然首先打印,因为它是层次结构中最顶层的类,但是 SailMixin 消息和 FlutterMixin 消息的顺序发生了变化。
我们甚至可以在一个类中多次包含同一个 mixin:
class QuickBird extends Bird with FlutterMixin, FlutterMixin {}
Bird ▶︎ The bird is moving
FlutterMixin ▶︎ The bird is fluttering through the sky
FlutterMixin ▶︎ The bird is fluttering through the sky
何时使用 Mixin
现在我们看到 mixins 可以成为跨多个类共享属性和行为的非常有用的工具,让我们看一下 mixins TickerProviderStateMixin 和 SingleTickerProviderStateMixin。两者都用于 Flutter 中的显式动画。通过在我们的小部件定义中指定这些 mixin 之一,我们告诉 Flutter 小部件是动画的,并且我们希望收到帧更新的通知,以便我们的 AnimationController 可以生成一个新值,并且我们可以为下一个动画步骤重绘小部件。
顾名思义,当你只有一个动画控制器时使用 SingleTickerProviderStateMixin,当你有多个动画控制器时使用 TickerProviderStateMixin。
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T>
implements TickerProvider {
Ticker? _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
// ...
_ticker = Ticker(
onTick,
debugLabel:
kDebugMode ? 'created by ${describeIdentity(this)}' : null
);
return _ticker!;
}
@override
void didChangeDependencies() {
if (_ticker != null)
_ticker!.muted = !TickerMode.of(context);
super.didChangeDependencies();
}
// ...
}
SingleTickerProviderStateMixin 使用了 Flutter State-lifecycle 方法。如果我们现在更深入地了解一下,我们还会看到在 SingleTickerProviderStateMixin 继承 StatelessWidget 来触发重绘。
在 Dart 和 Flutter 中大量使用 mixin 的另一个领域是 JSON(反)序列化。手动编写方法来序列化和反序列化数据类通常容易出错并且是重复的任务。这就是为什么许多开发人员求助于代码生成工具来创建该功能的原因之一。使用 mixins,生成的代码可以很容易地包含在原始数据类中。
还有很多 mixins 非常有用的例子,例如:
服务定位器 i18n(国际化/本地化) 日志记录
缺点和危险
一个主要问题(与任何编程技术一样)是过度使用:如果开始仅依赖 mixins 而没有“旧的”继承,那么您的类很快就会变成这样:
class Creature with FlutterMixin, SailMixin, ReadMixin, EatMixin,....
换句话说:Mixins 使得在一个类中包含各种功能非常诱人。但是当一个类可以做的事情太多时,它就违反了单一职责原则,并且很难对类的目的保持一个概览和直观的理解。此外,从哪个 mixin 继承哪个方法可能会变得非常混乱,并且其他开发人员可能不清楚代码执行的顺序。
Mixin 不会取代继承。他们扩展它。他们修补了它的一些缺陷。明智地使用它们,并始终考虑将子类化作为替代方案。