Constructors

Constructors

构造函数是创建类实例的特殊函数。

Dart 实现了多种类型的构造函数。除了默认构造函数,这些函数的名称都与其所属的类名相同。

生成式构造函数 (Generative constructors)
创建新实例并初始化实例变量。

默认构造函数 (Default constructors)
当没有指定构造函数时,用于创建新实例。它不接受参数,也没有名称。

命名构造函数 (Named constructors)
阐明构造函数的用途,或允许为同一个类创建多个构造函数。

常量构造函数 (Constant constructors)
将实例创建为编译时常量。

工厂构造函数 (Factory constructors)
创建子类型的新实例或从缓存中返回现有实例。

重定向构造函数 (Redirecting constructor)
将调用转发给同一类中的另一个构造函数。

构造函数的类型

生成式构造函数

要实例化一个类,请使用生成式构造函数。

1
2
3
4
5
6
7
8
class Point {
// 用于存储点坐标的实例变量。
double x;
double y;

// 带有初始化形参的生成式构造函数:
Point(this.x, this.y);
}

默认构造函数

如果你没有声明构造函数,Dart 会使用默认构造函数。默认构造函数是一个没有参数、没有名称的生成式构造函数。

命名构造函数

使用命名构造函数可以为一个类实现多个构造函数,或为构造函数提供更清晰的语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const double xOrigin = 0;
const double yOrigin = 0;

class Point {
final double x;
final double y;

// 在构造函数体运行之前,
// 设置 x 和 y 实例变量。
Point(this.x, this.y);

// 命名构造函数
Point.origin() : x = xOrigin, y = yOrigin;
}

子类不会继承父类的命名构造函数。要在子类中创建一个在父类中定义的同名构造函数,必须在子类中实现该构造函数。

常量构造函数

如果你的类创建的对象是不可变的,可以将这些对象设为编译时常量。为此,需要定义一个 const 构造函数,并确保所有实例变量都是 final 的。

1
2
3
4
5
6
7
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);

final double x, y;

const ImmutablePoint(this.x, this.y);
}

常量构造函数并不总是创建常量。它们也可能在非 const 上下文中被调用。要了解更多信息,请参阅关于使用构造函数的部分。

重定向构造函数

一个构造函数可以重定向到同一个类中的另一个构造函数。重定向构造函数没有函数体,在冒号 (:) 之后使用 this 关键字而不是类名。

1
2
3
4
5
6
7
8
9
class Point {
double x, y;

// 这个类的主构造函数。
Point(this.x, this.y);

// 委托给主构造函数。
Point.alongXAxis(double x) : this(x, 0);
}

工厂构造函数

当实现一个构造函数时遇到以下两种情况之一,请使用 factory 关键字:

  1. 构造函数并不总是创建其类的新实例。虽然工厂构造函数不能返回 null,但它可能返回:
    • 一个从缓存中获取的现有实例,而不是创建一个新实例。
    • 一个子类型的新实例。
  2. 在构造实例之前需要执行一些复杂的逻辑。这可能包括检查参数或执行任何无法在初始化列表中处理的逻辑。

提示
你也可以使用 late final (请谨慎使用!) 来处理 final 变量的延迟初始化。

以下示例包含两个工厂构造函数。

  • Logger 工厂构造函数从缓存中返回对象。
  • Logger.fromJson 工厂构造函数从一个 JSON 对象初始化一个 final 变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Logger {
final String name;
bool mute = false;

// _cache 是库级私有的,因为
// 它的名称前面有下划线 _。
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
var logger = Logger('UI');
logger.log('Button clicked');

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

重定向工厂构造函数

重定向工厂构造函数指定了对另一个类构造函数的调用,每当有人调用该重定向构造函数时,都会使用这个指定的构造函数。

1
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable

看起来普通的工厂构造函数似乎也能创建并返回其他类的实例,这使得重定向工厂变得不必要。但重定向工厂有几个优点:

  • 一个抽象类可以提供一个常量构造函数,该构造函数使用另一个类的常量构造函数。
  • 重定向工厂构造函数避免了转发器(forwarders)需要重复形参及其默认值。

构造函数引用 (Tear-offs)

Dart 允许你在不调用的情况下,将构造函数作为参数传递。这种方式称为“引用(Tear-off)”(就像把括号撕掉一样),它就像一个闭包,可以用相同的参数调用该构造函数。

如果这个构造函数引用的签名和返回类型与方法接受的参数相匹配,你就可以将该引用作为参数或变量使用。

引用与 lambda 或匿名函数不同。Lambda 是对构造函数的包装,而引用本身就是构造函数。

使用引用 (Tear-Offs)

推荐

1
2
3
4
5
// 使用命名构造函数的引用:
var strings = charCodes.map(String.fromCharCode);

// 使用未命名构造函数的引用:
var buffers = charCodes.map(StringBuffer.new);

不要使用 Lambda

不推荐

1
2
3
4
5
// 不要为命名构造函数使用 lambda:
var strings = charCodes.map((code) => String.fromCharCode(code));

// 不要为未命名构造函数使用 lambda:
var buffers = charCodes.map((code) => StringBuffer(code));

更多讨论,请观看关于 tear-offs 的 Decoding Flutter 视频。

播放视频:Dart Tear-offs | Decoding Flutter

实例变量的初始化

Dart 可以通过三种方式初始化变量。

在声明中初始化实例变量

在声明实例变量时就进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
class PointA {
double x = 1.0;
double y = 2.0;

// 隐式的默认构造函数将这些变量设置为 (1.0, 2.0)
// PointA();

@override
String toString() {
return 'PointA($x,$y)';
}
}

使用初始化形参

为了简化将构造函数参数赋值给实例变量这一常见模式,Dart 提供了初始化形参。

在构造函数声明中,包含 this.<propertyName> 并省略函数体。this 关键字指向当前实例。

当存在名称冲突时,使用 this。否则,Dart 风格建议省略 this。一个例外是生成式构造函数,你必须在初始化形参名称前加上 this 前缀。

正如本指南前面提到的,某些构造函数和构造函数的某些部分无法访问 this。这些包括:

  • 工厂构造函数
  • 初始化列表的右侧
  • 父类构造函数的参数

初始化形参也允许你初始化非空或 final 的实例变量。这两种类型的变量都需要初始化或一个默认值。

1
2
3
4
5
6
7
8
9
10
11
class PointB {
final double x;
final double y;

// 在构造函数体运行之前,
// 设置 x 和 y 实例变量。
PointB(this.x, this.y);

// 初始化形参也可以是可选的。
PointB.optional([this.x = 0.0, this.y = 0.0]);
}

私有字段不能用作命名的初始化形参。

1
2
3
4
5
6
7
8
9
class PointB {
// ...

PointB.namedPrivate({required double x, required double y})
: _x = x,
_y = y;

// ...
}

这也适用于命名变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PointC {
double x; // 必须在构造函数中设置
double y; // 必须在构造函数中设置

// 带有默认值的初始化形参的生成式构造函数
PointC.named({this.x = 1.0, this.y = 1.0});

@override
String toString() {
return 'PointC.named($x,$y)';
}
}

// 使用命名变量的构造函数。
final pointC = PointC.named(x: 2.0, y: 2.0);

所有通过初始化形参引入的变量都是 final 的,并且只在被初始化的变量的作用域内有效。

要执行无法在初始化列表中表达的逻辑,可以创建一个工厂构造函数或静态方法来处理该逻辑,然后将计算出的值传递给一个普通的构造函数。

构造函数参数可以设置为可空类型,并且不进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
class PointD {
double? x; // 如果构造函数中未设置,则为 null
double? y; // 如果构造函数中未设置,则为 null

// 带有初始化形参的生成式构造函数
PointD(this.x, this.y);

@override
String toString() {
return 'PointD($x,$y)';
}
}

使用初始化列表

在构造函数体运行之前,你可以初始化实例变量。用逗号分隔多个初始化表达式。

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

警告
初始化列表的右侧无法访问 this

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

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

初始化列表有助于设置 final 字段。

以下示例在初始化列表中初始化了三个 final 字段。要执行代码,请点击“运行”。

构造函数的继承

子类不会继承其父类(即直接父类)的构造函数。如果一个类没有声明构造函数,它只能使用默认构造函数。

一个类可以继承父类的参数。这些被称为 **父类参数 (super parameters)**。

构造函数的运作方式与调用一连串静态方法有些相似。每个子类都可以调用其父类的构造函数来初始化一个实例,就像子类可以调用父类的静态方法一样。这个过程不会“继承”构造函数的函数体或签名。

非默认的父类构造函数

Dart 按以下顺序执行构造函数:

  1. 初始化列表
  2. 父类的未命名、无参数的构造函数
  3. 主类的无参数构造函数

如果父类没有未命名、无参数的构造函数,则必须调用父类中的一个构造函数。在构造函数体(如果有)之前,用冒号 (:) 指定父类构造函数。

在下面的例子中,Employee 类的构造函数调用了其父类 Person 的命名构造函数。要执行以下代码,请点击“运行”。

由于 Dart 在调用父类构造函数之前会先计算其参数,因此参数可以是一个表达式,比如一个函数调用。

1
2
3
4
class Employee extends Person {
Employee() : super.fromJson(fetchDefaultData());
// ···
}

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

父类参数 (Super parameters)

为了避免将每个参数都传递到构造函数的 super 调用中,可以使用父类初始化形参将参数转发给指定的或默认的父类构造函数。此功能不能与重定向构造函数一起使用。父类初始化形参的语法和语义与初始化形参类似。

版本说明
使用父类初始化形参需要 Dart 语言版本至少为 2.17。如果你使用的是更早的语言版本,则必须手动传入所有父类构造函数的参数。

如果父类构造函数的调用包含位置参数,则父类初始化形参不能是位置参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vector2d {
final double x;
final double y;

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

class Vector3d extends Vector2d {
final double z;

// 像这样将 x 和 y 参数转发给默认的父类构造函数:
// Vector3d(final double x, final double y, this.z) : super(x, y);
Vector3d(super.x, super.y, this.z);
}

为了进一步说明,请看以下示例。

1
2
3
// 如果你用任何位置参数调用了父类构造函数 (`super(0)`),
// 再使用父类参数 (`super.x`) 将会导致错误。
Vector3d.xAxisError(super.x): z = 0, super(0); // 错误

这个命名构造函数试图设置 x 值两次:一次在父类构造函数中,一次作为位置父类参数。由于两者都指向 x 这个位置参数,因此会导致错误。

当父类构造函数有命名参数时,你可以将它们分散在命名父类参数(下一个例子中的 super.y)和传递给父类构造函数调用的命名参数(super.named(x: 0))之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Vector2d {
// ...
Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
final double z;

// 像这样将 y 参数转发给命名的父类构造函数:
// Vector3d.yzPlane({required double y, required this.z})
// : super.named(x: 0, y: y);
Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}
作者

wuhunyu

发布于

2025-09-10

更新于

2025-09-11

许可协议