星的天空的博客

种一颗树,最好的时间是十年前,其次是现在。

0%

Dart中的extends, with, implements, on关键字详解

Dart中类的类型

Dart是支持基于mixin继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null以外的所有的类都继承自Object类。 基于mixin的继承意味着尽管每个类(top class Object? 除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。

以上这段是官方文档的说明,在实际使用中,由于mixin的加入,使得Dart中类的使用和其它语言有所不同。Dart中类的类型有三种,分别是:

  • class:声明一个类,提供具体的成员变量和方法实现。
  • abstract class:声明一个抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。
  • mixin:声明一个Mixin类,与抽象类一样无法被实例化,是一种在多重继承中复用某个类中代码的方法模式,可以声明接口方法或有具体的方法实现。
  1. 每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的成员变量以及这个类所实现的其它接口。
  2. 如果想让抽象类同时可被实例化,可以为其定义工厂构造函数。具体内容可以参考:抽象类的实例化
  3. mixin关键字在Dart 2.1中才被引用支持。早期版本中的代码通常使用 abstract class代替

从上述内容可以看出,mixin是后面才被引入的,与abstract class有些通用的地方,可以理解为abstract class的升级版。它相对于abstract class说,可以同时引入多个Mixin,并且可以通过on关键字来限制使用范围。

类相关关键字的使用

而对上述这些类型的使用,又有extends, with, implements, on这几个关键字:

  • extends:继承,和其它语言的继承没什么区别。
  • with:使用Mixin模式混入一个或者多个Mixin类
  • implements:实现一个或多个接口并实现每个接口定义的API。
  • on:限制Mixin的使用范围。

针对这几个关键字的使用,我做了一张表进行总结:

样例说明

针对上面的内容,我举几个例子,可以复制代码到DartPad中进行验证:

类混入类或者抽象类(class with class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal {
String name = "Animal";
}
abstract class Flyer {
String name = "Flyer";
void fly() => print('$name can fly!');
}
abstract class Eater extends Animal {
void eat() => print('I can Eat!');
}

// 同时混入class和abstract class
abstract class Bird with Animal, Flyer {}
class Bird1 with Animal, Flyer {}

// 只支持无任何继承和混入的类,Eater继承自Animal,所以它不支持被混入。
// 报错:The class 'Eater' can't be used as a mixin because it extends a class other than 'Object'.
// class Bird with Eater {
// }

main() {
Bird1().fly(); // Flyer can fly!
}

类继承抽象类并混入Mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Animal {
String name = "Animal";
}

mixin Flyer {
String name = "Flyer";
void fly() => print('$name can fly!');
}

abstract class Eater extends Animal {
@override
String get name => "Eater";
void eat() => print('$name can Eat!');
}

// 类继承抽象类并混入Mixin
class Bird extends Eater with Flyer { }

main() {
// 因为with(混入)的优先级比extends(继承)更高,所以打印出来的是Flyer而不是Eater
Bird().fly(); // Flyer can fly!
Bird().eat(); // Flyer can Eat!
}

类继承抽象类并混入Mixin的同时实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Biology {
void breathe() => print('I can breathe');
}

class Animal {
String name = "Animal";
}

// 这里设置实现了Biology接口,但是mixin与abstract class一样并不要求实现接口,声明与实现均可。
// on关键字限制混入Flyer的类必须继承自Animal或它的子类
mixin Flyer on Animal implements Biology {
@override
String get name => "Flyer";
void fly() => print('$name can fly!');
}

abstract class Eater extends Animal {
@override
String get name => "Eater";
void eat() => print('$name can Eat!');
}

// 类继承抽象类并混入Mixin的同时实现接口
// 注意关键字的使用顺序,依次是extends -> with -> implements
class Bird extends Eater with Flyer implements Biology {
// 后面使用了`implements Biology`,所以子类必须要实现这个类的接口
@override
void breathe() => print('Bird can breathe!');
}

main() {
// 因为with(混入)的优先级比extends(继承)更高,所以打印出来的是Flyer而不是Eater
Bird().fly(); // Flyer can fly!
Bird().eat(); // Flyer can Eat!
Bird().breathe(); // Bird can breathe!
}

混入mixin的顺序问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
abstract class Biology {
void breathe() => print('I can breathe');
}

mixin Animal on Biology {
String name = "Animal";
@override
void breathe() {
print('$name can breathe!');
super.breathe();
}
}

mixin Flyer on Animal {
@override
String get name => "Flyer";
void fly() => print('$name can fly!');
}

/// mixin的顺序问题:
/// with后面的Flyer必须在Animal后面,否则会报错:
/// 'Flyer' can't be mixed onto 'Biology' because 'Biology' doesn't implement 'Animal'.
class Bird extends Biology with Animal, Flyer {
@override
void breathe() {
print('Bird can breathe!');
super.breathe();
}
}

main() {
Bird().breathe();
/*
* 上述代码执行,依次输出:
* Bird can breathe!
* Flyer can breathe!
* I can breathe
* */
}

这里的顺序问题和运行出来的结果会让人有点费解,但是可以这样理解:Mixin语法糖, 本质还是类继承. 继承可以复用代码, 但多继承会导致代码混乱。 Java为了解决多继承的问题, 用了interface, 只有函数声明而没有实现(后面加的default也算语法糖了)。以A with B, C, D为例,实际是A extends D extends C extends B, 所以上面的Animal必须在Flyer前面,否则就变成了Animal extends Flyer,会出现儿子给爹当爹的混乱问题。

mixin的底层本质只是猜测,并没有查看语言底层源码进行验证.

总结

从上述样例可以看出,三种类结构可以同时存在,关键字的使用有前后顺序:extends -> mixins -> implements
另外需要注意的是相同方法的优先级问题,这个有两种情况:

  1. 同时被extendswithimplements时,混入(with)的优先级比继承(extends)要高,而implements只提供接口,不会被调用。
  2. with多个Mixin时,则会调用距离with关键字最远Mixin中的方法。

当然,如果当前使用类重写了该方法,就会优先调用当前类中的方法。

参考资料