一、Dart基础目录:

1.1 思维导图

image.png

1.2 Dart基础将分五篇讲解:

主要讲解关键字、变量、内置类型、操作符、控制流程语句
主要讲解函数
主要讲解类
主要讲解泛型、库及可见性
主要讲解异步支持、异常

二、类

Dart 是一种基于类和 mixin 继承机制的面向对象的语言。 每个对象都是一个类的实例,所有的类都继承于 Object.基于 Mixin 继承 意味着每个类(除 Object 外) 都只有一个超类, 一个类中的代码可以在其他多个继承类中重复使用。

2.1 类的成员变量

对象是由函数和数据(即方法和实例变量)组成。 方法的调用要通过对象来完成: 调用的方法可以访问其对象的其他函数和数据,使用 (.) 来引用实例对象的变量和方法:

  1. var p = Point(2, 2);
  2. p.y = 3;
  3. assert(p.y == 3);
  4. // 调用 p 的 distanceTo() 方法。
  5. num distance = p.distanceTo(Point(4, 4));
  6. // 使用 ?. 来代替 . , 可以避免因为左边对象可能为 null , 导致的异常:
  7. // 如果 p 为 non-null,设置它变量 y 的值为 4。
  8. p?.y = 4;

2.2 构造函数

2.2.1 使用构造函数

通过 构造函数 创建对象, 构造函数的名字可以是 _ClassName_ 或者 _ClassName_._identifier_。例如, 以下代码使用 PointPoint.fromJson() 构造函数创建 Point 对象:

  1. var p1 = Point(2, 2);
  2. var p2 = Point.fromJson({'x': 1, 'y': 2});

以下代码具有相同的效果, 但是构造函数前面的的 new 关键字是可选的:

  1. var p1 = new Point(2, 2);
  2. var p2 = new Point.fromJson({'x': 1, 'y': 2});

版本提示: 在 Dart 2 中 new 关键字变成了可选的。

一些类提供了常量构造函数。 使用常量构造函数,在构造函数名之前加 const 关键字(更多可查看基础一),来创建编译时常量时:

  1. var p = const ImmutablePoint(2, 2);

构造两个相同的编译时常量会产生一个唯一的, 标准的实例:

  1. var a = const ImmutablePoint(1, 1);
  2. var b = const ImmutablePoint(1, 1);
  3. assert(identical(a, b)); // 它们是同一个实例。
  4. var a = const ImmutablePoint(2, 2);
  5. var b = const ImmutablePoint(1, 1);
  6. assert(identical(a, b)); // 它们是不是同一个实例,会报错。

常量上下文 中, 构造函数或者字面量前的 const 可以省略。 例如,下面代码创建了一个 const 类型的 map 对象:

  1. // 这里有很多的 const 关键字。
  2. const pointAndLine = const {
  3. 'point': const [const ImmutablePoint(0, 0)],
  4. 'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
  5. };

保留第一个 const 关键字,其余的全部省略:

  1. // 仅有一个 const ,由该 const 建立常量上下文。
  2. const pointAndLine = {
  3. 'point': [ImmutablePoint(0, 0)],
  4. 'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
  5. };

如果常量构造函数在常量上下文之外, 且省略了 const 关键字, 此时创建的对象是非常量对象:

  1. var a = const ImmutablePoint(1, 1); // 创建一个常量对象
  2. var b = ImmutablePoint(1, 1); // 创建一个非常量对象
  3. assert(!identical(a, b)); // 两者不是同一个实例!

版本提示: 在 Dart 2 中,一个常量上下文中的 const 关键字可以被省略。

通过创建一个与其类同名的函数来声明构造函数 (另外,还可以附加一个额外的可选标识符,如 命名构造函数 中所述)。 下面通过最常见的构造函数形式, 即生成构造函数, 创建一个类的实例:

  1. class Point {
  2. num x, y;
  3. Point(num x, num y) {
  4. // 还有更好的方式来实现下面代码,敬请关注。
  5. this.x = x;
  6. this.y = y;
  7. }
  8. }

使用 this 关键字引用当前实例。

提示: 近当存在命名冲突时,使用 this 关键字。 否则,按照 Dart 风格应该省略 this

通常模式下,会将构造函数传入的参数的值赋值给对应的实例变量, Dart 自身的语法糖精简了这些代码

  1. class Point {
  2. num x, y;
  3. // 在构造函数体执行前,
  4. // 语法糖已经设置了变量 x 和 y。
  5. Point(this.x, this.y);
  6. }

Dart构造函数有种实现方式:

  • 默认构造方法
  • 命名构造方法_ClassName_._identifier_
  • 调用父类构造方法
  • 重定向构造函数
  • 常量构造函数
  • 工厂构造函数:factory

    2.2.2 默认构造函数

    在没有声明构造函数的情况下, Dart 会提供一个默认的构造函数。 默认构造函数没有参数并会调用父类的无参构造函数。


    2.2.3 构造函数不被继承

    子类不会继承父类的构造函数。 子类不会继承父类的无名有参构造函数和命名构造函数(即子类只能继承父类无名、无参数的构造函数),父类构造函数会在子类的构造函数前调用,子类不声明构造函数,那么它就只有默认构造函数 (匿名,没有参数) 。 ```dart class Person { String firstName;

    // 无参数的,非命名的构造函数 Person() {

    1. print('in Person');

    } }

    class Son extends Person { // 因为父类有无参数的,非命名的构造函数,所以可以不用手动调用父类的构造函数
    Son.fromDictionary(Map data) {

    1. print('in Son');

    } }

// 注意:你无法调用Son(),原因在于你没有定义该构造方法; // 输出: // in Person // in Son

  1. 如果父类不显示提供无名无参的构造函数,在子类中必须手动调用父类的一个构造函数。这种情况下,调用父类的构造函数的代码放在子类构造函数名后,子类构造函数体前,中间使用 : 分隔
  2. ```dart
  3. class Person {
  4. String firstName;
  5. // 命名构造函数
  6. Person.fromDictionary(Map data) {
  7. print('in Person');
  8. }
  9. }
  10. class Son extends Person {
  11. // 父类没有无参数的,非命名的构造函数,所以必须手动调用一个父类的构造函数
  12. Son.fromDictionary(Map data) : super.fromDictionary(data) {
  13. print('in Son');
  14. }
  15. // 下面即使使用无参构造函数,也必须手动调用父类的构造函数;
  16. // 除非父类存在无参构造函数,这样一来上面的fromDictionary也无需调用父类构造函数;
  17. // 这里就可以理解为子类默认是调用了父类的无参构造函数,所以就无需手动调用;
  18. Son(): super.fromDictionary({}) {
  19. print('defalut in Son');
  20. }
  21. // fixme 这种写法会报错,因为父类没有无参数的,非命名的构造函数
  22. Son.fromDictionary(Map data) {
  23. print('in Son');
  24. }
  25. }


2.2.4 命名构造函数

使用命名构造函数可为一个类实现多个构造函数, 也可以使用命名构造函数来更清晰的表明函数意图:

  1. class Point {
  2. num x, y;
  3. Point(this.x, this.y);
  4. // 命名构造函数
  5. Point.origin() {
  6. x = 0;
  7. y = 0;
  8. }
  9. }

切记,构造函数不能够被继承, 这意味着父类的命名构造函数不会被子类继承。 如果希望使用父类中定义的命名构造函数创建子类, 就必须在子类中实现该构造函数。


2.2.5 调用父类非默认构造函数

默认情况下,子类的构造函数会自动调用父类的默认构造函数(匿名,无参数)。 父类的构造函数在子类构造函数体开始执行的位置被调用。 如果提供了一个 initializer list (初始化参数列表), 则初始化参数列表在父类构造函数执行之前执行。 总之,执行顺序如下:

  1. initializer list (初始化参数列表)
  2. superclass’s no-arg constructor (父类的无名构造函数)
  3. main class’s no-arg constructor (主类的无名构造函数)

如果父类中没有匿名无参的构造函数, 则需要手工调用父类的其他构造函数。 在当前构造函数冒号 (:) 之后,函数体之前,声明调用父类构造函数。
下面的示例中,Employee 类的构造函数调用了父类 Person 的命名构造函数。
由于父类的构造函数参数在构造函数执行之前执行, 所以参数可以是一个表达式或者一个方法调用:

  1. class Person {
  2. String firstName;
  3. Person.fromJson(Map data) {
  4. print('in Person');
  5. }
  6. }
  7. class Employee extends Person {
  8. // Person does not have a default constructor;
  9. // you must call super.fromJson(data).
  10. Employee.fromJson(Map data) : super.fromJson(data) {
  11. print('in Employee');
  12. }
  13. }
  14. main() {
  15. var emp = new Employee.fromJson({});
  16. if (emp is Person) {
  17. // Type check
  18. emp.firstName = 'Bob';
  19. }
  20. (emp as Person).firstName = 'Bob';
  21. }
  22. // 输出
  23. // in Person
  24. // in Employee

警告: 调用父类构造函数的参数无法访问 this 。 例如,参数可以为静态函数但是不能是实例函数。


2.2.6 初始化列表

除了调用超类构造函数之外, 还可以在构造函数体执行之前初始化实例变量。 各参数的初始化用逗号分隔。

  1. // 在构造函数体执行之前,
  2. // 通过初始列表设置实例变量。
  3. Point.fromJson(Map<String, num> json)
  4. : x = json['x'],
  5. y = json['y'] {
  6. print('In Point.fromJson(): ($x, $y)');
  7. }

警告: 初始化程序的右侧无法访问 this

在开发期间, 可以使用 assert 来验证输入的初始化列表。

  1. Point.withAssert(this.x, this.y) : assert(x >= 0) {
  2. print('In Point.withAssert(): ($x, $y)');
  3. }

使用初始化列表可以很方便的设置 final 字段。 下面示例演示了,如何使用初始化列表初始化设置三个 final 字段。

  1. import 'dart:math';
  2. class Point {
  3. final num x;
  4. final num y;
  5. final num distanceFromOrigin;
  6. Point(x, y)
  7. : x = x,
  8. y = y,
  9. distanceFromOrigin = sqrt(x * x + y * y);
  10. }
  11. main() {
  12. var p = new Point(2, 3);
  13. print(p.distanceFromOrigin);
  14. }

我们再举个例子来说明初始化列表、父类、子类的调用顺序

  1. class Person {
  2. String firstName;
  3. Person(String name) : firstName = 'person' {
  4. print('in Person : $firstName');
  5. firstName = name;
  6. print('in Person : $firstName');
  7. }
  8. }
  9. class Son extends Person {
  10. Son(): super('son') {
  11. print('defalut in Son');
  12. }
  13. }
  14. void main() {
  15. Son son = Son();
  16. // in Person : person
  17. // in Person : son
  18. // defalut in Son
  19. }

2.2.7 重定向构造函数

有时构造函数的唯一目的是重定向到同一个类中的另一个构造函数。 重定向构造函数的函数体为空, 构造函数的调用在冒号 (:) 之后。

  1. class Point {
  2. num x, y;
  3. // 类的主构造函数。
  4. Point(this.x, this.y);
  5. // 指向主构造函数
  6. Point.alongXAxis(num x) : this(x, 0);
  7. }

这里总结下,构造函数冒号 (:) 之后的用途有(无法一起使用)

  • 调用父类非默认构造函数
  • 初始化列表参数
  • 重定向构造函数

    2.2.8 常量构造函数

    如果该类生成的对象是固定不变的, 那么就可以把这些对象定义为编译时常量。 为此,需要定义一个 const 构造函数, 并且声明所有实例变量为 final

    1. class ImmutablePoint {
    2. static final ImmutablePoint origin =
    3. const ImmutablePoint(0, 0);
    4. final num x, y;
    5. const ImmutablePoint(this.x, this.y);
    6. }

    常量构造函数创建的实例并不总是常量。 更多内容,查看构造函数那一小节。


    2.2.9 工厂构造函数

    当执行构造函数并不总是创建这个类的一个新实例时,则使用 factory 关键字。 例如,一个工厂构造函数可能会返回一个 cache 中的实例, 或者可能返回一个子类的实例。
    以下示例演示了从缓存中返回对象的工厂构造函数:

    1. class Logger {
    2. final String name;
    3. bool mute = false;
    4. // 从命名的 _ 可以知, _cache 是私有属性。
    5. static final Map<String, Logger> _cache = <String, Logger>{};
    6. factory Logger(String name) {
    7. if (_cache.containsKey(name)) {
    8. return _cache[name];
    9. } else {
    10. final logger = Logger._internal(name);
    11. _cache[name] = logger;
    12. return logger;
    13. }
    14. }
    15. Logger._internal(this.name);
    16. void log(String msg) {
    17. if (!mute) print(msg);
    18. }
    19. }

    提示: 工厂构造函数无法访问 this。

工厂构造函的调用方式与其他构造函数一样:

  1. var logger = Logger('UI');
  2. logger.log('Button clicked');

补充说明:借助工厂构造函数能够实现单例;

  1. // 使用工厂构造实现简单单例
  2. class DioUtil {
  3. static final DioUtil _instance = DioUtil._init();
  4. static Dio _dio;
  5. factory DioUtil() {
  6. return _instance;
  7. }
  8. DioUtil._init() {
  9. _dio = new Dio();
  10. }
  11. }
  12. // GlobalEventBus.instance 或者是 GlobalEventBus()调用,同一实例
  13. class GlobalEventBus {
  14. EventBus eventBus;
  15. factory GlobalEventBus() => _getInstance();
  16. static GlobalEventBus get instance => _getInstance();
  17. static GlobalEventBus _instance;
  18. GlobalEventBus._internal() {
  19. // 创建对象
  20. eventBus = EventBus();
  21. }
  22. static GlobalEventBus _getInstance() {
  23. if (_instance == null) {
  24. _instance = GlobalEventBus._internal();
  25. }
  26. return _instance;
  27. }
  28. }

2.3 获取对象类型

使用对象的 runtimeType 属性, 可以在运行时获取对象的类型, runtimeType 属性回返回一个 Type 对象。

  1. print('The type of a is ${a.runtimeType}');


2.4 实例变量

下面是声明实例变量的示例:

  1. class Point {
  2. num x; // 声明示例变量 x,初始值为 null 。
  3. num y; // 声明示例变量 y,初始值为 null 。
  4. num z = 0; // 声明示例变量 z,初始值为 0 。
  5. }

未初始化实例变量的默认人值为 “null” 。
所有实例变量都生成隐式 getter 方法。 非 final 的实例变量同样会生成隐式 setter 方法, 有关更多信息,参考方法里的Getters 和 setters.

  1. class Point {
  2. num x;
  3. num y;
  4. }
  5. void main() {
  6. var point = Point();
  7. point.x = 4; // Use the setter method for x.
  8. assert(point.x == 4); // Use the getter method for x.
  9. assert(point.y == null); // Values default to null.
  10. }

如果在声明时进行了示例变量的初始化, 那么初始化值会在示例创建时赋值给变量, 该赋值过程在构造函数及其初始化列表执行之前。

2.5 方法

方法是为对象提供行为的函数。

2.5.1 实例方法

对象的实例方法可以访问 this 和实例变量。 以下示例中的 distanceTo() 方法就是实例方法:

  1. import 'dart:math';
  2. class Point {
  3. num x, y;
  4. Point(this.x, this.y);
  5. num distanceTo(Point other) {
  6. var dx = x - other.x;
  7. var dy = y - other.y;
  8. return sqrt(dx * dx + dy * dy);
  9. }
  10. }
  11. 2

2.5.2 Getter 和 Setter

Getter 和 Setter 是用于对象属性读和写的特殊方法。 回想之前的例子,每个实例变量都有一个隐式 Getter ,通常情况下还会有一个 Setter 。 使用 getset 关键字实现 Getter 和 Setter ,能够为实例创建额外的属性。

  1. class Rectangle {
  2. num left, top, width, height;
  3. Rectangle(this.left, this.top, this.width, this.height);
  4. // 定义两个计算属性: right 和 bottom。
  5. num get right => left + width;
  6. set right(num value) => left = value - width;
  7. num get bottom => top + height;
  8. set bottom(num value) => top = value - height;
  9. }
  10. void main() {
  11. var rect = Rectangle(3, 4, 20, 15);
  12. assert(rect.left == 3);
  13. rect.right = 12;
  14. assert(rect.left == -8);
  15. }

最开始实现 Getter 和 Setter 也许是直接返回成员变量; 随着需求变化, Getter 和 Setter 可能需要进行计算处理而使用方法来实现; 但是,调用对象的代码不需要做任何的修改。

提示: 类似 (++) 之类操作符不管是否定义了 getter 方法,都能够正确的执行。 为了避免一些问题,操作符只调用一次 getter 方法, 然后把值保存到一个临时的变量中。

2.5.3 抽象方法

实例方法, getter 和 setter 方法可以是抽象的, 只定义接口不进行实现,而是留给其他类去实现。 抽象方法只存在于 抽象类 中。
定义一个抽象函数,使用分号 (;) 来代替函数体:

  1. abstract class Doer {
  2. // 定义实例变量和方法 ...
  3. void doSomething(); // 定义一个抽象方法。
  4. }
  5. class EffectiveDoer extends Doer {
  6. void doSomething() {
  7. // 提供方法实现,所以这里的方法就不是抽象方法了...
  8. }
  9. }

调用抽象方法会导致运行时错误。

2.6 抽象类

使用 abstract 修饰符来定义 抽象类 — 抽象类不能实例化。 抽象类通常用来定义接口,以及部分实现。 如果希望抽象类能够被实例化,那么可以通过定义一个 工厂构造函数 来实现。

  1. abstract class User {
  2. String name;
  3. //默认构造方法
  4. User(this.name);
  5. //工厂方法返回Child实例
  6. factory User.test(String name){
  7. return new Child(name);
  8. }
  9. void printName();
  10. }
  11. // extends 继承抽象类
  12. class Child extends User{
  13. Child(String name) : super(name);
  14. @override
  15. void printName() {
  16. print(name);
  17. }
  18. }
  19. void main() {
  20. var p = User.test("黄药师");
  21. print(p.runtimeType); //输出实际类型 Child
  22. p.printName();//输出实际类型 黄药师
  23. }

抽象类通常具有 抽象方法。 下面是一个声明具有抽象方法的抽象类示例:

  1. // 这个类被定义为抽象类,所以不能被实例化。
  2. abstract class AbstractContainer {
  3. // 定义构造行数,字段,方法...
  4. void updateChildren(); // 抽象方法。
  5. }


2.7 隐式接口

每个类都隐式的定义了一个接口(Dart 没有像 Java 用单独的关键字 interface 来定义接口,普通用 class 声明的类就可以是接口),接口包含了该类所有的实例成员及其实现的接口。 如果要创建一个 A 类,A 要支持 B 类的 API ,但是不需要继承 B 的实现, 那么可以通过 A 实现 B 的接口。
一个类可以通过 implements 关键字来实现一个或者多个接口, 并实现每个接口要求的 API。 例如:

  1. // person 类。 隐式接口里面包含了 greet() 方法声明。
  2. class Person {
  3. // 包含在接口里,但只在当前库中可见。
  4. final _name;
  5. // 不包含在接口里,因为这是一个构造函数。
  6. Person(this._name);
  7. // 包含在接口里。
  8. String greet(String who) => 'Hello, $who. I am $_name.';
  9. }
  10. // person 接口的实现。
  11. class Impostor implements Person {
  12. get _name => '';
  13. String greet(String who) => 'Hi $who. Do you know who I am?';
  14. }
  15. String greetBob(Person person) => person.greet('Bob');
  16. void main() {
  17. print(greetBob(Person('Kathy')));
  18. print(greetBob(Impostor()));
  19. // Hello, Bob. I am Kathy.
  20. // Hi Bob. Do you know who I am?
  21. }

下面示例演示一个类如何实现多个接口: Here’s an example of specifying that a class implements multiple interfaces:

  1. class Point implements Comparable, Location {...}

2.8 扩展类(继承)

2.8.1 使用 extends 关键字来创建子类, 使用 super 关键字来引用父类:
  1. class Television {
  2. void turnOn() {
  3. _illuminateDisplay();
  4. _activateIrSensor();
  5. }
  6. // ···
  7. }
  8. class SmartTelevision extends Television {
  9. void turnOn() {
  10. super.turnOn();
  11. _bootNetworkInterface();
  12. _initializeMemory();
  13. _upgradeApps();
  14. }
  15. // ···
  16. }


2.8.2 重写类成员

子类可以重写实例方法,getter 和 setter。 可以使用 @override 注解指出想要重写的成员:

  1. class SmartTelevision extends Television {
  2. @override
  3. void turnOn() {...}
  4. // ···
  5. }


2.8.3 重写运算符

下标的运算符可以被重写。 例如,想要实现两个向量对象相加,可以重写 + 方法。

< + ` ` []
> / ^ []=
<= ~/ & ~
>= * << ==
% >>

提示: 你可能会被提示 != 运算符为非可重载运算符。 因为 e1 != e2 表达式仅仅是 !(e1 == e2) 的语法糖。

下面示例演示一个类重写 +- 操作符:

  1. class Vector {
  2. final int x, y;
  3. Vector(this.x, this.y);
  4. Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  5. Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
  6. // 运算符 == 和 hashCode 部分没有列出。 有关详情,请参考下面的注释。
  7. // ···
  8. }
  9. void main() {
  10. final v = Vector(2, 3);
  11. final w = Vector(2, 2);
  12. assert(v + w == Vector(4, 5));
  13. assert(v - w == Vector(0, 1));
  14. }

如果要重写 == 操作符,需要重写对象的 hashCode getter 方法。 重写 ==hashCode 的实例,参考 Implementing map keys

2.8.4 noSuchMethod()

当代码尝试使用不存在的方法或实例变量时, 通过重写 noSuchMethod() 方法,来实现检测和应对处理:

  1. class A {
  2. // 如果不重写 noSuchMethod,访问
  3. // 不存在的实例变量时会导致 NoSuchMethodError 错误。
  4. @override
  5. void noSuchMethod(Invocation invocation) {
  6. print('You tried to use a non-existent member: ' +
  7. '${invocation.memberName}');
  8. }
  9. }

除非符合下面的任意一项条件, 否则没有实现的方法不能够被调用:

  • receiver 具有 dynamic 的静态类型 。
  • receiver 具有静态类型,用于定义为实现的方法 (可以是抽象的), 并且 receiver 的动态类型具有 noSuchMethod() 的实现, 该实现与 Object 类中的实现不同。

有关更多信息,参考 noSuchMethod forwarding specification.

2.9 枚举类型

枚举类型也称为 enumerations 或 enums , 是一种特殊的类,用于表示数量固定的常量值
使用 enum 关键字定义一个枚举类型:

  1. enum Color { red, green, blue }

枚举中的每个值都有一个 index getter 方法, 该方法返回值所在枚举类型定义中的位置(从 0 开始)。 例如,第一个枚举值的索引是 0 , 第二个枚举值的索引是 1。

  1. assert(Color.red.index == 0);
  2. assert(Color.green.index == 1);
  3. assert(Color.blue.index == 2);

使用枚举的 values 常量, 获取所有枚举值列表( list )。

  1. List<Color> colors = Color.values;
  2. assert(colors[2] == Color.blue);

可以在 switch 语句 中使用枚举, 如果不处理所有枚举值,会收到警告:

  1. var aColor = Color.blue;
  2. switch (aColor) {
  3. case Color.red:
  4. print('Red as roses!');
  5. break;
  6. case Color.green:
  7. print('Green as grass!');
  8. break;
  9. default: // 没有这个,会看到一个警告。
  10. print(aColor); // 'Color.blue'
  11. }

枚举类型具有以下限制:

  • 枚举不能被子类化,混合或实现。
  • 枚举不能被显式实例化。

有关更多信息,参考 Dart language specification


2.10 为类添加功能:Mixin

Mixin 是复用类代码的一种途径, 复用的类可以在不同层级,之间可以不存在继承关系。
通过 with 后面跟一个或多个混入的名称,来 使用 Mixin , 下面的示例演示了两个使用 Mixin 的类:

  1. class Musician extends Performer with Musical {
  2. // ···
  3. }
  4. class Maestro extends Person
  5. with Musical, Aggressive, Demented {
  6. Maestro(String maestroName) {
  7. name = maestroName;
  8. canConduct = true;
  9. }
  10. }

通过创建一个继承自 Object 且没有构造函数的类,来 实现 一个 Mixin 。 如果 Mixin 不希望作为常规类被使用,使用关键字 mixin 替换 class 。 例如:

  1. mixin Musical {
  2. bool canPlayPiano = false;
  3. bool canCompose = false;
  4. bool canConduct = false;
  5. void entertainMe() {
  6. if (canPlayPiano) {
  7. print('Playing piano');
  8. } else if (canConduct) {
  9. print('Waving hands');
  10. } else {
  11. print('Humming to self');
  12. }
  13. }
  14. }

指定只有某些类型可以使用的 Mixin - 比如, Mixin 可以调用 Mixin 自身没有定义的方法 - 使用 on 来指定可以使用 Mixin 的父类类型:

  1. mixin MusicalPerformer on Musician {
  2. // ···
  3. }


版本提示: mixin 关键字在 Dart 2.1 中被引用支持。 早期版本中的代码通常使用 abstract class 代替。 更多有关 Mixin 在 2.1 中的变更信息,请参见 Dart SDK changelog2.1 mixin specification提示: 对 Mixin 的一些限制正在被移除。 关于更多详情,参考 proposed mixin specification.

有关 Dart 中 Mixin 的理论演变,参考 A Brief History of Mixins in Dart.

2.10.1 为什么我们需要Mixin

我们先来看看下面的类继承图:

Dart 基础三:类 - 图2

我们这里有一个名为Animal的超类,它有三个子类(Mammal,Bird和Fish)。在底部,我们有具体的一些子类。
小方块代表行为。例如,蓝色方块表示具有此行为的类的实例可以swim。
有些动物有共同的行为:猫和鸽子都可以行走,但是猫不能飞。
这些行为与此分类正交,因此我们无法在超类中实现这些行为。
如果一个类可以拥有多个超类,那就很容易办到了。我们可以创建另外三个类:Walker,Swimmer,Flyer。在那之后,我们只需从Walker类继承Dove和Cat。但在Dart中,每个类(除了Object类)都只有一个超类。
我们可以实现它,而不是继承自Walker类,因为它是一个接口,但我们必须在多个类中实现行为,因此它并不是一个好的解决方案。
我们需要一种在多个类层次结构中重用类的代码的方法。
Mixin就能够办到这一点!

‘Mixins are a way of reusing a class’s code in multiple class hierarchies. — dartlang.org’

我们可以通过java来实现该继承方式,如下:

  1. public class Animal {...}
  2. public class Mammal extends Animal {...}
  3. public class Bird extends Animal {...}
  4. public class Fish extends Animal {...}
  5. public class Dolphin extends Mammal {...}
  6. public class Bat extends Mammal {...}
  7. public class Cat extends Mammal {...}
  8. public class Dove extends Bird {...}
  9. public class Duck extends Bird {...}
  10. public class Shark extends Fish {...}
  11. public class FlyingFish extends Fish {...}

根据上图来分别给这些类添加行为:Walk,Swim及Flying,由于这些行为并不是所有类通用的,所以不能将这些行为放在父类。但如果把这三个行为分别放在三个类中,然后让其他类来继承这三个类,也就可以解决上述问题。但这样就是多继承,而Java又不支持多继承。所以这时候凸显出接口的重要性,通过接口来实现上述行为。代码如下:

  1. // 行走行为
  2. public interface Walk {
  3. void walk();
  4. }
  5. // 游泳行为
  6. public interface Swim {
  7. void swim();
  8. }
  9. // 飞翔行为
  10. public interface Flying {
  11. void flying();
  12. }
  13. // 海豚可以游泳
  14. public class Dolphin extends Mammal implements Swim {
  15. @Override
  16. public void swim() {...}
  17. }
  18. // 蝙蝠可以飞、行走
  19. public class Bat extends Mammal implements Flying,Walk {
  20. @Override
  21. public void walk() {...}
  22. @Override
  23. public void flying() {...}
  24. }
  25. // 猫可以行走
  26. public class Cat extends Mammal implements Walk {
  27. @Override
  28. public void walk() {...}
  29. }
  30. // 鸽子可以行走、飞
  31. public class Dove extends Bird implements Walk,Flying {
  32. @Override
  33. public void walk() {...}
  34. @Override
  35. public void flying() {...}
  36. }
  37. // 鸭子可以行走、飞及游泳
  38. public class Duck extends Bird implements Walk,Flying,Swim {
  39. @Override
  40. public void swim() {...}
  41. @Override
  42. public void walk() {...}
  43. @Override
  44. public void flying() {...}
  45. }
  46. // 鲨鱼可以游泳
  47. public class Shark extends Fish implements Swim {
  48. @Override
  49. public void swim() {...}
  50. }
  51. // 飞鱼可以游泳、飞
  52. public class FlyingFish extends Fish implements Swim,Flying {
  53. @Override
  54. public void swim() {...}
  55. @Override
  56. public void flying() {...}
  57. }

在Java中通过接口给类添加了行为,同理在Dart中一样可以给类添加行为实现上述功能,虽然Dart中没有interface关键字,但Dart中是有接口的概念且任意类都可以作为接口,代码几乎和java的一样。但在Dart中,我们还可以用Mixin来实现上述需求。通过Mixin将上面的一些行为加入到各自对应的类中。下面来代码实现:

  1. // 行走
  2. mixin Walker{
  3. void walk(){...}
  4. }
  5. // 游泳
  6. mixin Swim{
  7. void swim(){...}
  8. }
  9. // 飞翔
  10. mixin Flying {
  11. // 由于这个是抽象方法,所以必须要实现,不能调用super.flying()
  12. void flying();
  13. }
  14. // 海豚可以游泳
  15. class Dolphin extends Mammal with Swim{
  16. @override
  17. void swim() {
  18. super.swim();
  19. }
  20. }
  21. // 蝙蝠可以飞、行走
  22. class Bat extends Mammal with Flying,Walk{
  23. @override
  24. void flying() {...}
  25. //覆盖Walk类中的walk方法
  26. @override
  27. void walk() {
  28. super.walk();
  29. }
  30. }
  31. // 猫可以行走,这里没有重写Walk中的方法
  32. class Cat extends Mammal with Walk{}
  33. // 鸽子可以行走、飞
  34. class Dove extends Bird with Flying,Walk{
  35. @override
  36. void flying() {...}
  37. }
  38. // 鸭子可以行走、飞及游泳
  39. class Duck extends Bird with Walk,Flying,Swim{
  40. @override
  41. void flying() {...}
  42. @override
  43. void walk() {...}
  44. }
  45. // 鲨鱼可以游泳
  46. class Shark extends Fish with Swim{...}
  47. // 飞鱼可以飞及游泳
  48. class FlyingFish extends Fish with Flying,Swim{
  49. @override
  50. void flying() {...}
  51. }

咋一看,这不就是将implement替换成了with,abstract class替换成了mixin嘛,也太简单了。但仔细一看,我们发现mixin里有方法的具体实现,这样可以避免接口的方法必须在子类实现从而导致的代码冗余(Java 8通过关键字default也可以做到这一点)问题。简而言之,mixin相对于接口能够更好的避免代码冗余,使代码更加集中。

2.10.2 Mixins的定义及创建

mixins 的中文意思是混入,就是在类中混入其他功能。
Dart中的定义是:

Mixins are a way of reusing a class’s code in multiple class hierarchies. Mixins是一种在多个类层次结构中复用类代码的方法。

可以看出Mixins最重要的功能是复用代码,我们先看下JAVA,复用代码的方式有哪些:

  1. 继承
    子类可以复用父类的方法和属性,但是JAVA里的继承只能单继承。
  2. 组合
    将要复用的代码,封装成类A,让其他类持有A的实例,看上去貌似解决了复用代码的问题,但是一方面,每个类持有的A的实例是不同的,有多少个类,就总共有多少个A的实例,而且另一方面,即使A使用单例,使用起来也很不方便。
  3. 接口
    定义一个接口interface,类实现interface,这样虽然接口是同一个,但是实现却是分散的,能复用的代码是有限的。

所以在JAVA里想要复用代码,限制是很多的,这就有了mixins的概念,mixins最早的根源来自于Lisp,因为Dart也受到smalltakk的影响,所以Dart引入了mixins的概念,在维基百科中有对mixins最准确的定义:

在面向对象的语言中,mixins类是一个可以把自己的方法提供给其他类使用,但却不需要成为其他类的父类。

mixins是要通过非继承的方式来复用类中的代码。
**
通过创建一个继承自 Object 且没有构造函数的类,来实现一个Mixin, 如果 Mixin 不希望作为常规类被使用,使用关键字 mixin 替换 class ,换句话说,mixin也可以使用class关键字定义(也可以是抽象类),也可以当做普通class一样使用,mixins通过普通的类声明隐式定义:

  1. class Walker {
  2. void walk() {
  3. print("I'm walking");
  4. }
  5. }

如果我们不想让我们创建的mixin被实例化或扩展,我们可以像这样定义它:

  1. abstract class Walker {
  2. // This class is intended to be used as a mixin, and should not be
  3. // extended directly.
  4. factory Walker._() => null;
  5. void walk() {
  6. print("I'm walking");
  7. }
  8. }

要使用mixin的话,你需要使用with关键字,后跟一个或多个mixin的名称:

  1. class Cat extends Mammal with Walker {}
  2. class Dove extends Bird with Walker, Flyer {}

我在Cat类上定义了Walker mixin,它允许我们调用walk方法而不是fly方法(在Flyer中定义)。

  1. main(List<String> arguments) {
  2. Cat cat = Cat();
  3. Dove dove = Dove();
  4. // A cat can walk.
  5. cat.walk();
  6. // A dove can walk and fly.
  7. dove.walk();
  8. dove.fly();
  9. // A normal cat cannot fly.
  10. // cat.fly(); // Uncommenting this does not compile.
  11. }

可以使用 on 来指定可以使用 Mixin 的父类类型:

  1. mixin MusicalPerformer on Musician {
  2. // ···
  3. }

2.10.3 Mixins的线性化

mixin 到目前为止它看上去并不那么难是吗?
哈哈,那么,你能告诉我们以下程序的输出是什么吗?

  1. mixin A {
  2. String _name = 'A';
  3. String getMessage() => 'A';
  4. }
  5. mixin B {
  6. String _name = 'B';
  7. String getMessage() => 'B';
  8. }
  9. class P {
  10. String _name = 'P';
  11. String getMessage() => 'P';
  12. }
  13. class AB extends P with A, B {}
  14. class BA extends P with B, A {}
  15. void main() {
  16. String result = '';
  17. AB ab = AB();
  18. result += ab.getMessage();
  19. print('name : ${ab._name} \n');
  20. BA ba = BA();
  21. result += ba.getMessage();
  22. print(result);
  23. print('name : ${ba._name}');
  24. }

AB和BA类都使用A和B mixins继承至P类,但顺序不同。
所有的A,B和P类都有一个名为getMessage的方法和_name变量,首先,我们调用AB类的getMessage方法和变量,然后调用BA类的getMessage方法和变量,得出结果:

name : B BA name : A


我想你在猜测mixins的声明顺序非常重要。

在上面的示例中,我们发现with关键字后有多个类。那么这里就产生了一个问题——如果with后的多个类中有相同的方法,那么当调用该方法时,会调用哪个类里的方法呢?由于距离with关键字越远的类会重写前面类中的相同方法,因此分为以下两种情况

  • 如果当前使用类重写了该方法,就会调用当前类中的方法。
  • 如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。

所以当您将mixin混入类中时,请记住下面这句话:

Dart中的Mixins通过创建一个新类来实现,该类将mixin的实现层叠在一个超类之上以创建一个新类 ,它不是“在超类中”,而是在超类的“顶部”,因此如何解决查找问题不会产生歧义。 — Lasse R. H. Nielsen on StackOverflow.’

实际上,这段代码

  1. class AB extends P with A, B {}
  2. class BA extends P with B, A {}

在语义上等同于

  1. class PA = P with A;
  2. class PAB = PA with B;
  3. class AB extends PAB {}
  4. class PB = P with B;
  5. class PBA = PB with A;
  6. class BA extends PBA {}


最终的继承关系可以用下图表示:

Dart 基础三:类 - 图3

在AB和P之间创建新类,这些新类是超类P与A类和B类之间的混合类。

正如你所看到的那样,我们并没有使用多重继承!

  • Mixins不是一种在经典意义上获得多重继承的方法。
  • Mixins是一种抽象和重用一系列操作和状态的方法。
  • 它类似于扩展类所获得的重用,但它与单继承兼容,因为它是线性的。

— Lasse R. H. Nielsen on StackOverflow.

声明mixins的顺序代表了从最高级到最高级的继承链,这件事非常重要,你需要记住

再举个例子来证明上述继承关系是正确的:

  1. abstract class BindingBase {
  2. void initInstances() {
  3. print("BindingBase——initInstances");
  4. }
  5. }
  6. mixin GestureBinding on BindingBase {
  7. @override
  8. void initInstances() {
  9. print("GestureBinding——initInstances");
  10. super.initInstances();
  11. }
  12. }
  13. mixin RendererBinding on BindingBase {
  14. @override
  15. void initInstances() {
  16. print("RendererBinding——initInstances");
  17. super.initInstances();
  18. }
  19. }
  20. mixin WidgetsBinding on BindingBase {
  21. @override
  22. void initInstances() {
  23. print("WidgetsBinding——initInstances");
  24. super.initInstances();
  25. }
  26. }
  27. class WidgetsFlutterBinding extends BindingBase
  28. with GestureBinding, RendererBinding, WidgetsBinding {
  29. static WidgetsBinding ensureInitialized() {
  30. return WidgetsFlutterBinding();
  31. }
  32. }
  33. main(List<String> arguments) {
  34. var binding = WidgetsFlutterBinding();
  35. binding.initInstances();
  36. }

WidgetsFlutterBinding中并没有重写initInstances方法,那么就以最右边重写该方法的类——WidgetsBinding为主。那么结果应该如下。

WidgetsBinding——initInstances

但其真实结果其实如下:

WidgetsBinding——initInstances RendererBinding——initInstances GestureBinding——initInstances BindingBase——initInstances

这是为什么呢?仔细一点就可以发现,我们在WidgetsBinding、RendererBinding及GestureBinding中都调用了父类的initInstances方法,也因此会一级一级往上调用。如果我们取消该句代码,则会终止这种调用方式。比如做如下修改。

  1. mixin WidgetsBinding on BindingBase {
  2. @override
  3. void initInstances() {
  4. print("WidgetsBinding——initInstances");
  5. }
  6. }

其他代码保持不变,这样就是我们前面预想的结果了。

WidgetsBinding——initInstances

依次类推…
最后在用Java代码展示一下上面的继承关系。

  1. public class BindingBase {
  2. void initInstances() {
  3. System.out.printf("BindingBase——initInstances");
  4. }
  5. }
  6. public class GestureBinding extends BindingBase {
  7. @Override
  8. void initInstances() {
  9. // super.initInstances();
  10. }
  11. }
  12. public class RendererBinding extends GestureBinding {
  13. @Override
  14. void initInstances() {
  15. // super.initInstances();
  16. }
  17. }
  18. public class WidgetsBinding extends RendererBinding {
  19. @Override
  20. void initInstances() {
  21. // super.initInstances();
  22. }
  23. }
  24. public class WidgetsFlutterBinding extends WidgetsBinding {
  25. }

2.10.4 Mixins类型

通常,它是其超类的子类型,也是mixin名称本身表示的类的子类型,即原始类的类型。 — dartlang.org

mixins的类型就是其超类的子类型,所以这意味着这个程序将在控制台中打印六行true。

  1. class A {
  2. String getMessage() => 'A';
  3. }
  4. class B {
  5. String getMessage() => 'B';
  6. }
  7. class P {
  8. String getMessage() => 'P';
  9. }
  10. class AB extends P with A, B {}
  11. class BA extends P with B, A {}
  12. void main() {
  13. AB ab = AB();
  14. print(ab is P); // true
  15. print(ab is A); // true
  16. print(ab is B); // true
  17. BA ba = BA();
  18. print(ba is P); // true
  19. print(ba is A); // true
  20. print(ba is B); // true
  21. }

**
Lasse R. H. Nielsen给了我们一个很棒的解释:由于每个mixin应用程序都创建一个新类,它还会创建一个新接口(因为所有Dart类也定义了接口),如上所述,新类扩展了超类并包含了mixin类成员的副本,但它也实现了mixin类接口。

2.10.5 Mixins的使用场景及注意事项

当我们想要在不共享相同类层次结构的多个类之间共享行为时,或者在超类中实现此类行为没有意义时,Mixins非常有用。通常情况下是序列化(例如,查看jaguar_serializer)或持久化,但是你也可以使用mixins来提供一些实用功能(比如Flutter中的RenderSliverHelpers),不要局限于无状态mixins,你绝对可以存储变量并使用它们!

注意事项一:
mixin类要么是直接继承 object,要么是直接或间接继承 extends 关键字后的类,所以我们要清楚mixin到底是继承那个类。

在前面的示例上做一些修改。

  1. //一个新类
  2. abstract class Binding {
  3. void initInstances() {
  4. print("Binding——initInstances");
  5. }
  6. }
  7. abstract class BindingBase {
  8. void initInstances() {
  9. print("BindingBase——initInstances");
  10. }
  11. }
  12. mixin GestureBinding on Binding {
  13. @override
  14. void initInstances() {
  15. print("GestureBinding——initInstances");
  16. super.initInstances();
  17. }
  18. }
  19. mixin RendererBinding on BindingBase {
  20. @override
  21. void initInstances() {
  22. print("RendererBinding——initInstances");
  23. super.initInstances();
  24. }
  25. }
  26. mixin WidgetsBinding on BindingBase {
  27. @override
  28. void initInstances() {
  29. print("WidgetsBinding——initInstances");
  30. super.initInstances();
  31. }
  32. }
  33. class WidgetsFlutterBinding extends BindingBase
  34. with RendererBinding, WidgetsBinding, GestureBinding {
  35. static WidgetsBinding ensureInitialized() {
  36. return WidgetsFlutterBinding();
  37. }
  38. }

这时候我们就会发现代码报错,出现了如下警告。
image.png
当我们再次让 GestureBinding 继承自 BindingBase 时,上面错误就消失了。所以我们要清楚 mixin 到底是继承那个类,否则就可能会出现上述错误。

注意事项二:
如果类A实现了接口C,类B继承了接口C,那么类B一定得在类A的后面。
**

  1. //接口
  2. abstract class HitTestable {
  3. void hitTest(String msg);
  4. }
  5. //实现接口HitTestable
  6. mixin GestureBinding on BindingBase implements HitTestable {
  7. @override
  8. void initInstances() {
  9. print("GestureBinding——initInstances");
  10. super.initInstances();
  11. }
  12. @override
  13. void hitTest(String msg) {
  14. print("GestureBinding——hitTest:$msg");
  15. }
  16. }
  17. mixin RendererBinding on BindingBase, GestureBinding, HitTestable {
  18. @override
  19. void hitTest(String msg) {
  20. print("RendererBinding hitTest:");
  21. super.hitTest(msg);
  22. print("RendererBinding——hitTest:$msg");
  23. }
  24. }

那么如果想要在 with 后加上类 GestureBinding 及 RendererBinding,则必须 GestureBinding 在RendererBinding 的前面,否则会报错。

image.png
当我们让 GestureBinding 在 RendererBinding 前面后,该错误就会消失。

最后整理下,mixins弥补了接口和继承的不足,继承只能单继承,而接口无法复用实现,mixins却可以多混入并且能利用到混入类。关于Mixins,还有很多需要注意的事情,我们虽然可以使用Mixins对代码进行一些简化,但是要建立在对需求和类之间的关系准确理解的基础上,建议多去看看Flutter中使用Mixins实现的一些源码,从里面吸取一些正确的经验。

2.11 类变量和方法

使用 static 关键字实现类范围的变量和方法。

2.11.1 静态变量

静态变量(类变量)对于类级别的状态是非常有用的:

  1. class Queue {
  2. static const initialCapacity = 16;
  3. // ···
  4. }
  5. void main() {
  6. assert(Queue.initialCapacity == 16);
  7. }

静态变量只到它们被使用的时候才会初始化。

提示: 代码准守风格推荐指南 中的命名规则, 使用 lowerCamelCase 来命名常量。

2.11.2 静态方法

静态方法(类方法)不能在实例上使用,因此它们不能访问 this 。 例如:

  1. import 'dart:math';
  2. class Point {
  3. num x, y;
  4. Point(this.x, this.y);
  5. static num distanceBetween(Point a, Point b) {
  6. var dx = a.x - b.x;
  7. var dy = a.y - b.y;
  8. return sqrt(dx * dx + dy * dy);
  9. }
  10. }
  11. void main() {
  12. var a = Point(2, 2);
  13. var b = Point(4, 4);
  14. var distance = Point.distanceBetween(a, b);
  15. assert(2.8 < distance && distance < 2.9);
  16. print(distance);
  17. }


提示: 对于常见或广泛使用的工具和函数, 应该考虑使用顶级函数而不是静态方法。

静态函数可以当做编译时常量使用。 例如,可以将静态方法作为参数传递给常量构造函数。

参考资料:

  1. Dart 中文官网)
  2. Dart | 什么是Mixin
  3. Dart之Mixin详解)