一、Dart基础目录:
1.1 思维导图
1.2 Dart基础将分五篇讲解:
一 | 主要讲解关键字、变量、内置类型、操作符、控制流程语句 |
---|---|
二 | 主要讲解函数 |
三 | 主要讲解类 |
四 | 主要讲解泛型、库及可见性 |
五 | 主要讲解异步支持、异常 |
二、类
Dart 是一种基于类和 mixin 继承机制的面向对象的语言。 每个对象都是一个类的实例,所有的类都继承于 Object. 。 基于 Mixin 继承 意味着每个类(除 Object 外) 都只有一个超类, 一个类中的代码可以在其他多个继承类中重复使用。
2.1 类的成员变量
对象是由函数和数据(即方法和实例变量)组成。 方法的调用要通过对象来完成: 调用的方法可以访问其对象的其他函数和数据,使用 (.
) 来引用实例对象的变量和方法:
var p = Point(2, 2);
p.y = 3;
assert(p.y == 3);
// 调用 p 的 distanceTo() 方法。
num distance = p.distanceTo(Point(4, 4));
// 使用 ?. 来代替 . , 可以避免因为左边对象可能为 null , 导致的异常:
// 如果 p 为 non-null,设置它变量 y 的值为 4。
p?.y = 4;
2.2 构造函数
2.2.1 使用构造函数
通过 构造函数 创建对象, 构造函数的名字可以是 _ClassName_
或者 _ClassName_._identifier_
。例如, 以下代码使用 Point
和 Point.fromJson()
构造函数创建 Point
对象:
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
以下代码具有相同的效果, 但是构造函数前面的的 new
关键字是可选的:
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
版本提示: 在 Dart 2 中
new
关键字变成了可选的。
一些类提供了常量构造函数。 使用常量构造函数,在构造函数名之前加 const
关键字(更多可查看基础一),来创建编译时常量时:
var p = const ImmutablePoint(2, 2);
构造两个相同的编译时常量会产生一个唯一的, 标准的实例:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是同一个实例。
var a = const ImmutablePoint(2, 2);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是不是同一个实例,会报错。
在 常量上下文 中, 构造函数或者字面量前的 const
可以省略。 例如,下面代码创建了一个 const 类型的 map 对象:
// 这里有很多的 const 关键字。
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
保留第一个 const
关键字,其余的全部省略:
// 仅有一个 const ,由该 const 建立常量上下文。
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
如果常量构造函数在常量上下文之外, 且省略了 const
关键字, 此时创建的对象是非常量对象:
var a = const ImmutablePoint(1, 1); // 创建一个常量对象
var b = ImmutablePoint(1, 1); // 创建一个非常量对象
assert(!identical(a, b)); // 两者不是同一个实例!
版本提示: 在 Dart 2 中,一个常量上下文中的
const
关键字可以被省略。
通过创建一个与其类同名的函数来声明构造函数 (另外,还可以附加一个额外的可选标识符,如 命名构造函数 中所述)。 下面通过最常见的构造函数形式, 即生成构造函数, 创建一个类的实例:
class Point {
num x, y;
Point(num x, num y) {
// 还有更好的方式来实现下面代码,敬请关注。
this.x = x;
this.y = y;
}
}
使用 this
关键字引用当前实例。
提示: 近当存在命名冲突时,使用
this
关键字。 否则,按照 Dart 风格应该省略this
。
通常模式下,会将构造函数传入的参数的值赋值给对应的实例变量, Dart 自身的语法糖精简了这些代码:
class Point {
num x, y;
// 在构造函数体执行前,
// 语法糖已经设置了变量 x 和 y。
Point(this.x, this.y);
}
Dart构造函数有种实现方式:
- 默认构造方法
- 命名构造方法
_ClassName_._identifier_
- 调用父类构造方法
- 重定向构造函数
- 常量构造函数
-
2.2.2 默认构造函数
在没有声明构造函数的情况下, Dart 会提供一个默认的构造函数。 默认构造函数没有参数并会调用父类的无参构造函数。
2.2.3 构造函数不被继承
子类不会继承父类的构造函数。 子类不会继承父类的无名有参构造函数和命名构造函数(即子类只能继承父类无名、无参数的构造函数),父类构造函数会在子类的构造函数前调用,子类不声明构造函数,那么它就只有默认构造函数 (匿名,没有参数) 。 ```dart class Person { String firstName;
// 无参数的,非命名的构造函数 Person() {
print('in Person');
} }
class Son extends Person { // 因为父类有无参数的,非命名的构造函数,所以可以不用手动调用父类的构造函数
Son.fromDictionary(Map data) {print('in Son');
} }
// 注意:你无法调用Son(),原因在于你没有定义该构造方法; // 输出: // in Person // in Son
如果父类不显示提供无名无参的构造函数,在子类中必须手动调用父类的一个构造函数。这种情况下,调用父类的构造函数的代码放在子类构造函数名后,子类构造函数体前,中间使用 : 分隔
```dart
class Person {
String firstName;
// 命名构造函数
Person.fromDictionary(Map data) {
print('in Person');
}
}
class Son extends Person {
// 父类没有无参数的,非命名的构造函数,所以必须手动调用一个父类的构造函数
Son.fromDictionary(Map data) : super.fromDictionary(data) {
print('in Son');
}
// 下面即使使用无参构造函数,也必须手动调用父类的构造函数;
// 除非父类存在无参构造函数,这样一来上面的fromDictionary也无需调用父类构造函数;
// 这里就可以理解为子类默认是调用了父类的无参构造函数,所以就无需手动调用;
Son(): super.fromDictionary({}) {
print('defalut in Son');
}
// fixme 这种写法会报错,因为父类没有无参数的,非命名的构造函数
Son.fromDictionary(Map data) {
print('in Son');
}
}
2.2.4 命名构造函数
使用命名构造函数可为一个类实现多个构造函数, 也可以使用命名构造函数来更清晰的表明函数意图:
class Point {
num x, y;
Point(this.x, this.y);
// 命名构造函数
Point.origin() {
x = 0;
y = 0;
}
}
切记,构造函数不能够被继承, 这意味着父类的命名构造函数不会被子类继承。 如果希望使用父类中定义的命名构造函数创建子类, 就必须在子类中实现该构造函数。
2.2.5 调用父类非默认构造函数
默认情况下,子类的构造函数会自动调用父类的默认构造函数(匿名,无参数)。 父类的构造函数在子类构造函数体开始执行的位置被调用。 如果提供了一个 initializer list (初始化参数列表), 则初始化参数列表在父类构造函数执行之前执行。 总之,执行顺序如下:
- initializer list (初始化参数列表)
- superclass’s no-arg constructor (父类的无名构造函数)
- main class’s no-arg constructor (主类的无名构造函数)
如果父类中没有匿名无参的构造函数, 则需要手工调用父类的其他构造函数。 在当前构造函数冒号 (:
) 之后,函数体之前,声明调用父类构造函数。
下面的示例中,Employee 类的构造函数调用了父类 Person 的命名构造函数。
由于父类的构造函数参数在构造函数执行之前执行, 所以参数可以是一个表达式或者一个方法调用:
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Person does not have a default constructor;
// you must call super.fromJson(data).
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
if (emp is Person) {
// Type check
emp.firstName = 'Bob';
}
(emp as Person).firstName = 'Bob';
}
// 输出
// in Person
// in Employee
警告: 调用父类构造函数的参数无法访问 this 。 例如,参数可以为静态函数但是不能是实例函数。
2.2.6 初始化列表
除了调用超类构造函数之外, 还可以在构造函数体执行之前初始化实例变量。 各参数的初始化用逗号分隔。
// 在构造函数体执行之前,
// 通过初始列表设置实例变量。
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
警告: 初始化程序的右侧无法访问
this
。
在开发期间, 可以使用 assert
来验证输入的初始化列表。
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}
使用初始化列表可以很方便的设置 final 字段。 下面示例演示了,如何使用初始化列表初始化设置三个 final 字段。
import 'dart:math';
class Point {
final num x;
final num y;
final num distanceFromOrigin;
Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}
main() {
var p = new Point(2, 3);
print(p.distanceFromOrigin);
}
我们再举个例子来说明初始化列表、父类、子类的调用顺序
class Person {
String firstName;
Person(String name) : firstName = 'person' {
print('in Person : $firstName');
firstName = name;
print('in Person : $firstName');
}
}
class Son extends Person {
Son(): super('son') {
print('defalut in Son');
}
}
void main() {
Son son = Son();
// in Person : person
// in Person : son
// defalut in Son
}
2.2.7 重定向构造函数
有时构造函数的唯一目的是重定向到同一个类中的另一个构造函数。 重定向构造函数的函数体为空, 构造函数的调用在冒号 (:) 之后。
class Point {
num x, y;
// 类的主构造函数。
Point(this.x, this.y);
// 指向主构造函数
Point.alongXAxis(num x) : this(x, 0);
}
这里总结下,构造函数冒号 (:) 之后的用途有(无法一起使用)
- 调用父类非默认构造函数
- 初始化列表参数
-
2.2.8 常量构造函数
如果该类生成的对象是固定不变的, 那么就可以把这些对象定义为编译时常量。 为此,需要定义一个
const
构造函数, 并且声明所有实例变量为final
。class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
常量构造函数创建的实例并不总是常量。 更多内容,查看构造函数那一小节。
2.2.9 工厂构造函数
当执行构造函数并不总是创建这个类的一个新实例时,则使用
factory
关键字。 例如,一个工厂构造函数可能会返回一个 cache 中的实例, 或者可能返回一个子类的实例。
以下示例演示了从缓存中返回对象的工厂构造函数:class Logger {
final String name;
bool mute = false;
// 从命名的 _ 可以知, _cache 是私有属性。
static final Map<String, Logger> _cache = <String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
提示: 工厂构造函数无法访问 this。
工厂构造函的调用方式与其他构造函数一样:
var logger = Logger('UI');
logger.log('Button clicked');
补充说明:借助工厂构造函数能够实现单例;
// 使用工厂构造实现简单单例
class DioUtil {
static final DioUtil _instance = DioUtil._init();
static Dio _dio;
factory DioUtil() {
return _instance;
}
DioUtil._init() {
_dio = new Dio();
}
}
// GlobalEventBus.instance 或者是 GlobalEventBus()调用,同一实例
class GlobalEventBus {
EventBus eventBus;
factory GlobalEventBus() => _getInstance();
static GlobalEventBus get instance => _getInstance();
static GlobalEventBus _instance;
GlobalEventBus._internal() {
// 创建对象
eventBus = EventBus();
}
static GlobalEventBus _getInstance() {
if (_instance == null) {
_instance = GlobalEventBus._internal();
}
return _instance;
}
}
2.3 获取对象类型
使用对象的 runtimeType
属性, 可以在运行时获取对象的类型, runtimeType
属性回返回一个 Type 对象。
print('The type of a is ${a.runtimeType}');
2.4 实例变量
下面是声明实例变量的示例:
class Point {
num x; // 声明示例变量 x,初始值为 null 。
num y; // 声明示例变量 y,初始值为 null 。
num z = 0; // 声明示例变量 z,初始值为 0 。
}
未初始化实例变量的默认人值为 “null” 。
所有实例变量都生成隐式 getter 方法。 非 final 的实例变量同样会生成隐式 setter 方法, 有关更多信息,参考方法里的Getters 和 setters.
class Point {
num x;
num y;
}
void main() {
var point = Point();
point.x = 4; // Use the setter method for x.
assert(point.x == 4); // Use the getter method for x.
assert(point.y == null); // Values default to null.
}
如果在声明时进行了示例变量的初始化, 那么初始化值会在示例创建时赋值给变量, 该赋值过程在构造函数及其初始化列表执行之前。
2.5 方法
2.5.1 实例方法
对象的实例方法可以访问 this
和实例变量。 以下示例中的 distanceTo()
方法就是实例方法:
import 'dart:math';
class Point {
num x, y;
Point(this.x, this.y);
num distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
}
2
2.5.2 Getter 和 Setter
Getter 和 Setter 是用于对象属性读和写的特殊方法。 回想之前的例子,每个实例变量都有一个隐式 Getter ,通常情况下还会有一个 Setter 。 使用 get
和 set
关键字实现 Getter 和 Setter ,能够为实例创建额外的属性。
class Rectangle {
num left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算属性: right 和 bottom。
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}
最开始实现 Getter 和 Setter 也许是直接返回成员变量; 随着需求变化, Getter 和 Setter 可能需要进行计算处理而使用方法来实现; 但是,调用对象的代码不需要做任何的修改。
提示: 类似 (++) 之类操作符不管是否定义了 getter 方法,都能够正确的执行。 为了避免一些问题,操作符只调用一次 getter 方法, 然后把值保存到一个临时的变量中。
2.5.3 抽象方法
实例方法, getter 和 setter 方法可以是抽象的, 只定义接口不进行实现,而是留给其他类去实现。 抽象方法只存在于 抽象类 中。
定义一个抽象函数,使用分号 (;) 来代替函数体:
abstract class Doer {
// 定义实例变量和方法 ...
void doSomething(); // 定义一个抽象方法。
}
class EffectiveDoer extends Doer {
void doSomething() {
// 提供方法实现,所以这里的方法就不是抽象方法了...
}
}
调用抽象方法会导致运行时错误。
2.6 抽象类
使用 abstract
修饰符来定义 抽象类 — 抽象类不能实例化。 抽象类通常用来定义接口,以及部分实现。 如果希望抽象类能够被实例化,那么可以通过定义一个 工厂构造函数 来实现。
abstract class User {
String name;
//默认构造方法
User(this.name);
//工厂方法返回Child实例
factory User.test(String name){
return new Child(name);
}
void printName();
}
// extends 继承抽象类
class Child extends User{
Child(String name) : super(name);
@override
void printName() {
print(name);
}
}
void main() {
var p = User.test("黄药师");
print(p.runtimeType); //输出实际类型 Child
p.printName();//输出实际类型 黄药师
}
抽象类通常具有 抽象方法。 下面是一个声明具有抽象方法的抽象类示例:
// 这个类被定义为抽象类,所以不能被实例化。
abstract class AbstractContainer {
// 定义构造行数,字段,方法...
void updateChildren(); // 抽象方法。
}
2.7 隐式接口
每个类都隐式的定义了一个接口(Dart 没有像 Java 用单独的关键字 interface 来定义接口,普通用 class 声明的类就可以是接口),接口包含了该类所有的实例成员及其实现的接口。 如果要创建一个 A 类,A 要支持 B 类的 API ,但是不需要继承 B 的实现, 那么可以通过 A 实现 B 的接口。
一个类可以通过 implements
关键字来实现一个或者多个接口, 并实现每个接口要求的 API。 例如:
// person 类。 隐式接口里面包含了 greet() 方法声明。
class Person {
// 包含在接口里,但只在当前库中可见。
final _name;
// 不包含在接口里,因为这是一个构造函数。
Person(this._name);
// 包含在接口里。
String greet(String who) => 'Hello, $who. I am $_name.';
}
// person 接口的实现。
class Impostor implements Person {
get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
// Hello, Bob. I am Kathy.
// Hi Bob. Do you know who I am?
}
下面示例演示一个类如何实现多个接口: Here’s an example of specifying that a class implements multiple interfaces:
class Point implements Comparable, Location {...}
2.8 扩展类(继承)
2.8.1 使用 extends
关键字来创建子类, 使用 super
关键字来引用父类:
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}
2.8.2 重写类成员
子类可以重写实例方法,getter 和 setter。 可以使用 @override
注解指出想要重写的成员:
class SmartTelevision extends Television {
@override
void turnOn() {...}
// ···
}
2.8.3 重写运算符
下标的运算符可以被重写。 例如,想要实现两个向量对象相加,可以重写 +
方法。
< |
+ |
` | ` | [] |
---|---|---|---|---|
> |
/ |
^ |
[]= |
|
<= |
~/ |
& |
~ |
|
>= |
* |
<< |
== |
|
– |
% |
>> |
提示: 你可能会被提示
!=
运算符为非可重载运算符。 因为e1 != e2
表达式仅仅是!(e1 == e2)
的语法糖。
下面示例演示一个类重写 +
和 -
操作符:
class Vector {
final int x, y;
Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
// 运算符 == 和 hashCode 部分没有列出。 有关详情,请参考下面的注释。
// ···
}
void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);
assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}
如果要重写 ==
操作符,需要重写对象的 hashCode
getter 方法。 重写 ==
和 hashCode
的实例,参考 Implementing map keys
2.8.4 noSuchMethod()
当代码尝试使用不存在的方法或实例变量时, 通过重写 noSuchMethod()
方法,来实现检测和应对处理:
class A {
// 如果不重写 noSuchMethod,访问
// 不存在的实例变量时会导致 NoSuchMethodError 错误。
@override
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
}
除非符合下面的任意一项条件, 否则没有实现的方法不能够被调用:
- receiver 具有
dynamic
的静态类型 。 - receiver 具有静态类型,用于定义为实现的方法 (可以是抽象的), 并且 receiver 的动态类型具有
noSuchMethod()
的实现, 该实现与Object
类中的实现不同。
有关更多信息,参考 noSuchMethod forwarding specification.
2.9 枚举类型
枚举类型也称为 enumerations 或 enums , 是一种特殊的类,用于表示数量固定的常量值
使用 enum
关键字定义一个枚举类型:
enum Color { red, green, blue }
枚举中的每个值都有一个 index
getter 方法, 该方法返回值所在枚举类型定义中的位置(从 0 开始)。 例如,第一个枚举值的索引是 0 , 第二个枚举值的索引是 1。
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
使用枚举的 values
常量, 获取所有枚举值列表( list )。
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
可以在 switch 语句 中使用枚举, 如果不处理所有枚举值,会收到警告:
var aColor = Color.blue;
switch (aColor) {
case Color.red:
print('Red as roses!');
break;
case Color.green:
print('Green as grass!');
break;
default: // 没有这个,会看到一个警告。
print(aColor); // 'Color.blue'
}
枚举类型具有以下限制:
- 枚举不能被子类化,混合或实现。
- 枚举不能被显式实例化。
有关更多信息,参考 Dart language specification 。
2.10 为类添加功能:Mixin
Mixin 是复用类代码的一种途径, 复用的类可以在不同层级,之间可以不存在继承关系。
通过 with
后面跟一个或多个混入的名称,来 使用 Mixin , 下面的示例演示了两个使用 Mixin 的类:
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
通过创建一个继承自 Object 且没有构造函数的类,来 实现 一个 Mixin 。 如果 Mixin 不希望作为常规类被使用,使用关键字 mixin
替换 class
。 例如:
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
指定只有某些类型可以使用的 Mixin - 比如, Mixin 可以调用 Mixin 自身没有定义的方法 - 使用 on
来指定可以使用 Mixin 的父类类型:
mixin MusicalPerformer on Musician {
// ···
}
版本提示:
mixin
关键字在 Dart 2.1 中被引用支持。 早期版本中的代码通常使用abstract class
代替。 更多有关 Mixin 在 2.1 中的变更信息,请参见 Dart SDK changelog 和 2.1 mixin specification 。 提示: 对 Mixin 的一些限制正在被移除。 关于更多详情,参考 proposed mixin specification.
有关 Dart 中 Mixin 的理论演变,参考 A Brief History of Mixins in Dart.
2.10.1 为什么我们需要Mixin
我们先来看看下面的类继承图:
我们这里有一个名为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来实现该继承方式,如下:
public class Animal {...}
public class Mammal extends Animal {...}
public class Bird extends Animal {...}
public class Fish extends Animal {...}
public class Dolphin extends Mammal {...}
public class Bat extends Mammal {...}
public class Cat extends Mammal {...}
public class Dove extends Bird {...}
public class Duck extends Bird {...}
public class Shark extends Fish {...}
public class FlyingFish extends Fish {...}
根据上图来分别给这些类添加行为:Walk,Swim及Flying,由于这些行为并不是所有类通用的,所以不能将这些行为放在父类。但如果把这三个行为分别放在三个类中,然后让其他类来继承这三个类,也就可以解决上述问题。但这样就是多继承,而Java又不支持多继承。所以这时候凸显出接口的重要性,通过接口来实现上述行为。代码如下:
// 行走行为
public interface Walk {
void walk();
}
// 游泳行为
public interface Swim {
void swim();
}
// 飞翔行为
public interface Flying {
void flying();
}
// 海豚可以游泳
public class Dolphin extends Mammal implements Swim {
@Override
public void swim() {...}
}
// 蝙蝠可以飞、行走
public class Bat extends Mammal implements Flying,Walk {
@Override
public void walk() {...}
@Override
public void flying() {...}
}
// 猫可以行走
public class Cat extends Mammal implements Walk {
@Override
public void walk() {...}
}
// 鸽子可以行走、飞
public class Dove extends Bird implements Walk,Flying {
@Override
public void walk() {...}
@Override
public void flying() {...}
}
// 鸭子可以行走、飞及游泳
public class Duck extends Bird implements Walk,Flying,Swim {
@Override
public void swim() {...}
@Override
public void walk() {...}
@Override
public void flying() {...}
}
// 鲨鱼可以游泳
public class Shark extends Fish implements Swim {
@Override
public void swim() {...}
}
// 飞鱼可以游泳、飞
public class FlyingFish extends Fish implements Swim,Flying {
@Override
public void swim() {...}
@Override
public void flying() {...}
}
在Java中通过接口给类添加了行为,同理在Dart中一样可以给类添加行为实现上述功能,虽然Dart中没有interface关键字,但Dart中是有接口的概念且任意类都可以作为接口,代码几乎和java的一样。但在Dart中,我们还可以用Mixin来实现上述需求。通过Mixin将上面的一些行为加入到各自对应的类中。下面来代码实现:
// 行走
mixin Walker{
void walk(){...}
}
// 游泳
mixin Swim{
void swim(){...}
}
// 飞翔
mixin Flying {
// 由于这个是抽象方法,所以必须要实现,不能调用super.flying()
void flying();
}
// 海豚可以游泳
class Dolphin extends Mammal with Swim{
@override
void swim() {
super.swim();
}
}
// 蝙蝠可以飞、行走
class Bat extends Mammal with Flying,Walk{
@override
void flying() {...}
//覆盖Walk类中的walk方法
@override
void walk() {
super.walk();
}
}
// 猫可以行走,这里没有重写Walk中的方法
class Cat extends Mammal with Walk{}
// 鸽子可以行走、飞
class Dove extends Bird with Flying,Walk{
@override
void flying() {...}
}
// 鸭子可以行走、飞及游泳
class Duck extends Bird with Walk,Flying,Swim{
@override
void flying() {...}
@override
void walk() {...}
}
// 鲨鱼可以游泳
class Shark extends Fish with Swim{...}
// 飞鱼可以飞及游泳
class FlyingFish extends Fish with Flying,Swim{
@override
void flying() {...}
}
咋一看,这不就是将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,复用代码的方式有哪些:
- 继承
子类可以复用父类的方法和属性,但是JAVA里的继承只能单继承。 - 组合
将要复用的代码,封装成类A,让其他类持有A的实例,看上去貌似解决了复用代码的问题,但是一方面,每个类持有的A的实例是不同的,有多少个类,就总共有多少个A的实例,而且另一方面,即使A使用单例,使用起来也很不方便。 - 接口
定义一个接口interface,类实现interface,这样虽然接口是同一个,但是实现却是分散的,能复用的代码是有限的。
所以在JAVA里想要复用代码,限制是很多的,这就有了mixins
的概念,mixins
最早的根源来自于Lisp,因为Dart也受到smalltakk的影响,所以Dart引入了mixins
的概念,在维基百科中有对mixins
最准确的定义:
在面向对象的语言中,mixins类是一个可以把自己的方法提供给其他类使用,但却不需要成为其他类的父类。
mixins
是要通过非继承的方式来复用类中的代码。
**
通过创建一个继承自 Object 且没有构造函数的类,来实现一个Mixin, 如果 Mixin 不希望作为常规类被使用,使用关键字 mixin 替换 class ,换句话说,mixin也可以使用class关键字定义(也可以是抽象类),也可以当做普通class一样使用,mixins通过普通的类声明隐式定义:
class Walker {
void walk() {
print("I'm walking");
}
}
如果我们不想让我们创建的mixin被实例化或扩展,我们可以像这样定义它:
abstract class Walker {
// This class is intended to be used as a mixin, and should not be
// extended directly.
factory Walker._() => null;
void walk() {
print("I'm walking");
}
}
要使用mixin的话,你需要使用with关键字,后跟一个或多个mixin的名称:
class Cat extends Mammal with Walker {}
class Dove extends Bird with Walker, Flyer {}
我在Cat类上定义了Walker mixin,它允许我们调用walk方法而不是fly方法(在Flyer中定义)。
main(List<String> arguments) {
Cat cat = Cat();
Dove dove = Dove();
// A cat can walk.
cat.walk();
// A dove can walk and fly.
dove.walk();
dove.fly();
// A normal cat cannot fly.
// cat.fly(); // Uncommenting this does not compile.
}
可以使用 on
来指定可以使用 Mixin 的父类类型:
mixin MusicalPerformer on Musician {
// ···
}
2.10.3 Mixins的线性化
mixin 到目前为止它看上去并不那么难是吗?
哈哈,那么,你能告诉我们以下程序的输出是什么吗?
mixin A {
String _name = 'A';
String getMessage() => 'A';
}
mixin B {
String _name = 'B';
String getMessage() => 'B';
}
class P {
String _name = 'P';
String getMessage() => 'P';
}
class AB extends P with A, B {}
class BA extends P with B, A {}
void main() {
String result = '';
AB ab = AB();
result += ab.getMessage();
print('name : ${ab._name} \n');
BA ba = BA();
result += ba.getMessage();
print(result);
print('name : ${ba._name}');
}
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.’
实际上,这段代码
class AB extends P with A, B {}
class BA extends P with B, A {}
在语义上等同于
class PA = P with A;
class PAB = PA with B;
class AB extends PAB {}
class PB = P with B;
class PBA = PB with A;
class BA extends PBA {}
最终的继承关系可以用下图表示:
在AB和P之间创建新类,这些新类是超类P与A类和B类之间的混合类。
正如你所看到的那样,我们并没有使用多重继承!
- Mixins不是一种在经典意义上获得多重继承的方法。
- Mixins是一种抽象和重用一系列操作和状态的方法。
- 它类似于扩展类所获得的重用,但它与单继承兼容,因为它是线性的。
— Lasse R. H. Nielsen on StackOverflow.
声明mixins的顺序代表了从最高级到最高级的继承链,这件事非常重要,你需要记住。
再举个例子来证明上述继承关系是正确的:
abstract class BindingBase {
void initInstances() {
print("BindingBase——initInstances");
}
}
mixin GestureBinding on BindingBase {
@override
void initInstances() {
print("GestureBinding——initInstances");
super.initInstances();
}
}
mixin RendererBinding on BindingBase {
@override
void initInstances() {
print("RendererBinding——initInstances");
super.initInstances();
}
}
mixin WidgetsBinding on BindingBase {
@override
void initInstances() {
print("WidgetsBinding——initInstances");
super.initInstances();
}
}
class WidgetsFlutterBinding extends BindingBase
with GestureBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
return WidgetsFlutterBinding();
}
}
main(List<String> arguments) {
var binding = WidgetsFlutterBinding();
binding.initInstances();
}
WidgetsFlutterBinding中并没有重写initInstances方法,那么就以最右边重写该方法的类——WidgetsBinding为主。那么结果应该如下。
WidgetsBinding——initInstances
但其真实结果其实如下:
WidgetsBinding——initInstances RendererBinding——initInstances GestureBinding——initInstances BindingBase——initInstances
这是为什么呢?仔细一点就可以发现,我们在WidgetsBinding、RendererBinding及GestureBinding中都调用了父类的initInstances方法,也因此会一级一级往上调用。如果我们取消该句代码,则会终止这种调用方式。比如做如下修改。
mixin WidgetsBinding on BindingBase {
@override
void initInstances() {
print("WidgetsBinding——initInstances");
}
}
其他代码保持不变,这样就是我们前面预想的结果了。
WidgetsBinding——initInstances
依次类推…
最后在用Java
代码展示一下上面的继承关系。
public class BindingBase {
void initInstances() {
System.out.printf("BindingBase——initInstances");
}
}
public class GestureBinding extends BindingBase {
@Override
void initInstances() {
// super.initInstances();
}
}
public class RendererBinding extends GestureBinding {
@Override
void initInstances() {
// super.initInstances();
}
}
public class WidgetsBinding extends RendererBinding {
@Override
void initInstances() {
// super.initInstances();
}
}
public class WidgetsFlutterBinding extends WidgetsBinding {
}
2.10.4 Mixins类型
通常,它是其超类的子类型,也是mixin名称本身表示的类的子类型,即原始类的类型。 — dartlang.org
mixins的类型就是其超类的子类型,所以这意味着这个程序将在控制台中打印六行true。
class A {
String getMessage() => 'A';
}
class B {
String getMessage() => 'B';
}
class P {
String getMessage() => 'P';
}
class AB extends P with A, B {}
class BA extends P with B, A {}
void main() {
AB ab = AB();
print(ab is P); // true
print(ab is A); // true
print(ab is B); // true
BA ba = BA();
print(ba is P); // true
print(ba is A); // true
print(ba is B); // true
}
**
Lasse R. H. Nielsen给了我们一个很棒的解释:由于每个mixin应用程序都创建一个新类,它还会创建一个新接口(因为所有Dart类也定义了接口),如上所述,新类扩展了超类并包含了mixin类成员的副本,但它也实现了mixin类接口。
2.10.5 Mixins的使用场景及注意事项
当我们想要在不共享相同类层次结构的多个类之间共享行为时,或者在超类中实现此类行为没有意义时,Mixins非常有用。通常情况下是序列化(例如,查看jaguar_serializer)或持久化,但是你也可以使用mixins来提供一些实用功能(比如Flutter中的RenderSliverHelpers),不要局限于无状态mixins,你绝对可以存储变量并使用它们!
注意事项一:
mixin类要么是直接继承 object,要么是直接或间接继承 extends 关键字后的类,所以我们要清楚mixin到底是继承那个类。
在前面的示例上做一些修改。
//一个新类
abstract class Binding {
void initInstances() {
print("Binding——initInstances");
}
}
abstract class BindingBase {
void initInstances() {
print("BindingBase——initInstances");
}
}
mixin GestureBinding on Binding {
@override
void initInstances() {
print("GestureBinding——initInstances");
super.initInstances();
}
}
mixin RendererBinding on BindingBase {
@override
void initInstances() {
print("RendererBinding——initInstances");
super.initInstances();
}
}
mixin WidgetsBinding on BindingBase {
@override
void initInstances() {
print("WidgetsBinding——initInstances");
super.initInstances();
}
}
class WidgetsFlutterBinding extends BindingBase
with RendererBinding, WidgetsBinding, GestureBinding {
static WidgetsBinding ensureInitialized() {
return WidgetsFlutterBinding();
}
}
这时候我们就会发现代码报错,出现了如下警告。
当我们再次让 GestureBinding 继承自 BindingBase 时,上面错误就消失了。所以我们要清楚 mixin 到底是继承那个类,否则就可能会出现上述错误。
注意事项二:
如果类A实现了接口C,类B继承了接口C,那么类B一定得在类A的后面。
**
//接口
abstract class HitTestable {
void hitTest(String msg);
}
//实现接口HitTestable
mixin GestureBinding on BindingBase implements HitTestable {
@override
void initInstances() {
print("GestureBinding——initInstances");
super.initInstances();
}
@override
void hitTest(String msg) {
print("GestureBinding——hitTest:$msg");
}
}
mixin RendererBinding on BindingBase, GestureBinding, HitTestable {
@override
void hitTest(String msg) {
print("RendererBinding hitTest:");
super.hitTest(msg);
print("RendererBinding——hitTest:$msg");
}
}
那么如果想要在 with 后加上类 GestureBinding 及 RendererBinding,则必须 GestureBinding 在RendererBinding 的前面,否则会报错。
当我们让 GestureBinding 在 RendererBinding 前面后,该错误就会消失。
最后整理下,mixins弥补了接口和继承的不足,继承只能单继承,而接口无法复用实现,mixins却可以多混入并且能利用到混入类。关于Mixins,还有很多需要注意的事情,我们虽然可以使用Mixins对代码进行一些简化,但是要建立在对需求和类之间的关系准确理解的基础上,建议多去看看Flutter中使用Mixins实现的一些源码,从里面吸取一些正确的经验。
2.11 类变量和方法
2.11.1 静态变量
静态变量(类变量)对于类级别的状态是非常有用的:
class Queue {
static const initialCapacity = 16;
// ···
}
void main() {
assert(Queue.initialCapacity == 16);
}
静态变量只到它们被使用的时候才会初始化。
提示: 代码准守风格推荐指南 中的命名规则, 使用
lowerCamelCase
来命名常量。
2.11.2 静态方法
静态方法(类方法)不能在实例上使用,因此它们不能访问 this
。 例如:
import 'dart:math';
class Point {
num x, y;
Point(this.x, this.y);
static num distanceBetween(Point a, Point b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
}
void main() {
var a = Point(2, 2);
var b = Point(4, 4);
var distance = Point.distanceBetween(a, b);
assert(2.8 < distance && distance < 2.9);
print(distance);
}
提示: 对于常见或广泛使用的工具和函数, 应该考虑使用顶级函数而不是静态方法。
静态函数可以当做编译时常量使用。 例如,可以将静态方法作为参数传递给常量构造函数。
参考资料: