Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null 以外的所有的类都继承自 Object 类。基于 mixin 的继承 意味着尽管每个类 (top class Object? 除外) 都有一个超类,一个类的代码可以在其他多个类继承中重复使用。扩展方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。

类的成员

  • 对象的成员由函数和数据 (即 方法 和 实例变量)组成。方法的调用要通过对象来完成,这种方式可以访问对象的函数和数据。
  • 使用 (.)来访问对象的实例变量
1
2
3
4
var p = Point(2, 2);
assert(p.y == 2);

double distance = p.distanceTo(Point(4, 4));
  • 使用 ?. 代替 . 可以避免因为左边表达式为 null 而导致的问题
1
var a = p?.y;

对象的类型

1
2
3
4
5
// 可以使用 Object 对象的 runtimeType 属性在运行时获取一个对象的类型。

var a = const ImmutablePoint(0, 0);

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

实例变量

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
class Point {
double? x;
double? y;
double z = 0;
}

// 所有未初始化的实例变量其值均为 null。

// 所有实例变量均会隐式地声明一个 Getter 方法。非终值的实例变量和 late final 声明但未声明初始化的实例变量还会隐式地声明一个 Setter 方法。

var point = Point1();
point.x = 1;

// print(point.x == 1); true
// print(point.y == null); true

class Point1 {
double? x;
double? y;
}

// 实例变量可以是final,在这种情况下,它们必须恰好设置一次。在声明时,使用构造函数形参或构造函数的初始化列表初始化最终的非后期实例变量:

var mark = ProfileMark('11');
// mark.name = ''; 'name' can't be used as a setter because it's final.
// print(mark.name); 11

class ProfileMark {
final String name;
final DateTime start = DateTime.now();

ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}

构造函数

  • 声明一个与类名一样的函数即可声明一个构造函数 (对于命名式构造函数 还可以添加额外的标识符)。大部分构造函数形式是生成式构造函数,其用于创建一个类的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Point1 {
double x = 0;
double y = 0;

Point1(double x, double y) {
this.x = x;
this.y = y;
}
}

// 使用 this 关键字引用当前实例。
// 当且仅当命名冲突时使用 this 关键字才有意义,否则 Dart 会忽略 this 关键字。

  • 终值初始化
1
2
3
4
5
6
7
8
// 对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤。构造中初始化的参数可以用于初始化非空或 final 修饰的变量,它们都必须被初始化或提供一个默认值。

class Point1 {
double x = 0;
double y = 0;

Point1(this.x, this.y);
}
  • 默认构造函数:如果没有声明构造函数,那么Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的去参数构造方法。
  • 构造函数不被继承:子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。
  • 命名式构造函数
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
// 可以为一个类声明多个命名式构造函数来表达更明确的意图:

var p = Point1(2, 2);
var p1 = Point1.origin();
var p2 = Point1.test(2, 2);

const double xOrigin = 0;
const double yOrigin = 0;

class Point1 {
final double x;
final double y;

Point1(this.x, this.y);

Point1.origin()
: x = xOrigin,
y = yOrigin;

Point1.test(double x, double y)
: this.x = x,
this.y = y;
}

// 构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数,如果想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。
  • 调用父类非默认构造函数
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
41
42
// 默认情况下,子类的构造函数会调用父类的匿名无参数构造方法,并且该调用会在子类构造函数的函数体代码执行前,如果子类构造函数还有一个 初始化列表,那么该初始化列表会在调用父类的该构造函数之前被执行。调用顺序
// 1. 初始化列表
// 2. 父类的无参数构造函数
// 3. 当前类的构造函数

// 如果父类没有匿名无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用 (:) 指定。

var employee = Employee.fromJson({});

// print(employee);
// in Person
// in Employee
// Instance of 'Employee'




class Person {
String? firstName;

Person.fromJson(Map data) {
print('in Person');
}
}

class Employee extends Person {
Employee.fromJson(super.data) : super.fromJson() {
print("in Employee");
}
}

// 因为参会会在子类构造函数被执行前传递给父类的构造函数,因此该参数也可以是一个表达式,比如一个函数:

class Employee extends Person {
// Employee.fromJson(super.data) : super.fromJson() {
// print("in Employee");
// }

Employee() : super.fromJson({});
}

// 注意: 传递给父类构造函数的参数不能使用 this 关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员都不能被访问,但是类成员可以。
  • 超类参数
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
// 为了不重复的将参数传递到超类构造的指定参数,可以使用超类参数,直接在类的构造函数中使用超类构造的某个参数。超类参数不能和重定向参数一起使用。

class Vector2d {
final double x;
final double y;

Vector2d(this.x, this.y);
}

class Vector3d extends Vector2d {
final double z;

Vector3d(super.x, super.y, this.z);
}

// 如果超类构造的位置参数已被使用,那么超类构造函数就不能再继续使用被占用的位置。但是超类构造参数可以始终是命名参数:

class Vector2d {
final double x;
final double y;

Vector2d(this.x, this.y);

Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
final double z;

Vector3d(super.x, super.y, this.z);

Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}


  • 初始化列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 除了调用父类构造函数之外,还可以在构造函数执行之前初始化实例变量。
// 使用初始化列表设置 final 字段非常方便

class Point1 {
final double x;
final double y;
final double distanceFromOrigin;

Point1.test(double x, double y)
: this.x = x,
this.y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}

// 初始化列表表达式 = 右边的语句不能使用 this 关键字。

  • 重定向构造函数
1
2
3
4
5
6
7
8
9
// 有时候类中的构造函数仅用于调用类中其它的构造函数,此时该构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数 (使用 this 而非类名):

class Point {
double x, y;

Point(this.x, this.y);

Point.alongXAsis(double x) : this(x, 0);
}
  • 常量构造函数
1
2
3
4
5
6
7
8
9
// 如果类生成的对象都是不变的,可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能

class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);

final double x, y;

const ImmutablePoint(this.x, this.y);
}
  • 工厂构造函数
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
// 使用factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总会返回新的实例对象。例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类的实例

var logger = Logger('name');
// logger.log('msg'); msg

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);
// print(loggerJson.name); UI


class Logger {
final String name;
bool mute = false;

static final Map<String, Logger> _cache = <String, Logger>{};

factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}

factory Logger.fromJson(Map<String, Object> json) {
return Logger(json['name'].toString());
}

Logger._internal(this.name);

void log(String msg) {
if (!mute) print(msg);
}
}

// 在工厂构造函数中无法访问 this。

方法

  • 实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对象的实例方法可以访问实例变量和this

class Point {
final double x;
final double y;

Point(this.x, this.y);

double distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dy);
}
}
  • Getter 和 Setter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Getter 和 Setter 是一对用来读写对象属性的特殊方法,实例对象的每一个属性都有一个隐式的Getter方法,如果为非 final 属性的话还会有一个 Setter 方法,可以使用 get 和 set关键字为额外的属性添加 Getter 和 Setter 方法。

var rect = Rectangle(3, 4, 20, 15);
// print(rect.left); 3.0
// print(rect.right); 23.0
rect.right = 10;
// print(rect.left); -10.0



class Rectangle {
double left, top, width, height;

Rectangle(this.left, this.top, this.width, this.height);

double get right => left + width;
set right(double value) => left = value - width;
double get bottom => top + height;
set bottom(double value) => top = value - height;
}

// 使用Getter 和 Setter 好处是,可以先试用实例变量,过一段时间再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑。

// 像自增 (++) 这样的操作符不管是否定义了 Getter 方法都会正确地执行。为了避免一些不必要的异常情况。运算符只会调用 Getter 一次,然后将其值存储在一个临时的变量中。
  • 抽象方法
1
2
3
4
5
6
7
8
9
10
11
12
// 实例方法、Getter 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类区实现该方法,抽象方法只能存在于抽象类中。
// 直接使用分号 (;) 代替方法体即可声明一个抽象方法;

abstract class Doer {
void doSomething();
}

class EffectiveDoer extends Doer {
void doSomething() {
// TODO: implement doSomething
}
}

抽象类

1
2
3
4
5
// 使用关键字 abstract 标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时被实例化,可以为其定义 工厂构造函数。

abstract class Doer {
void doSomething();
}

隐式接口

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
// 每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。
// 一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:

// print(greetBob(Person('Kddd'))); Hello, Bob. I am Kddd
// print(greetBob(Impostor())); Hi Bob. Do you know who I am?


class Person {
final String _name;

Person(this._name);

String greet(String who) => 'Hello, $who. I am $_name';
}

class Impostor implements Person {
String get _name => '';

String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

// 如果需要实现多个类接口,可以使用逗号分割每个接口类:
class Point implements A, B { ... }