Skip to content

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

Dart 实现了许多类型的构造函数。除了默认构造函数之外,这些函数都使用与它们所属类相同的名称。

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

  • 默认构造函数 (Default constructors)
    当没有指定构造函数时使用。它是一个没有参数且没有名称的生成式构造函数。

  • 命名构造函数 (Named constructors)
    用于明确构造函数的目的,或者允许为同一个类创建多个构造函数。

  • 常量构造函数 (Constant constructors)
    创建在编译时就是常量的实例。

  • 工厂构造函数 (Factory constructors)
    要么创建一个子类的新实例,要么从缓存中返回一个已存在的实例。

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

  • 构造函数的类型 (Types of constructors)

生成式构造函数 (Generative constructors)

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

dart
class Point {
  // 用于保存点的坐标的实例变量。
  double x;
  double y;

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

默认构造函数 (Default constructors)

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

命名构造函数 (Named constructors)

使用命名构造函数可以为一个类实现多个构造函数,或者提供额外的清晰度:

dart
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;
}

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

常量构造函数 (Constant constructors)

如果你的类生成不可变的对象,可以让这些对象成为编译时的常量。要使对象成为编译时常量,你需要定义一个 const 构造函数,并将所有实例变量设置为 final

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

  final double x, y;

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

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

重定向构造函数 (Redirecting constructor)
一个构造函数可以重定向到同一个类中的另一个构造函数。重定向构造函数的函数体为空。该构造函数在冒号 (:) 后使用 this 而不是类名。

dart
class Point {
  double x, y;

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

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

工厂构造函数 (Factory constructors)

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

  1. 构造函数并不总是创建其类的新实例。虽然工厂构造函数不能返回 null,但它可能返回:

    • 从缓存中返回一个已存在的实例,而不是创建一个新的实例
    • 一个子类的新实例
  2. 在构建实例之前需要执行一些非平凡的工作。这可能包括检查参数或进行其他无法在初始化列表中完成的处理。

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

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

  • Logger 工厂构造函数从缓存中返回对象。
  • Logger.fromJson 工厂构造函数从 JSON 对象初始化一个 final 变量。
dart
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

你可以像使用其他构造函数一样使用工厂构造函数:

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

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

重定向工厂构造函数 (Redirecting factory constructors)
重定向工厂构造函数指定了当有人调用该重定向构造函数时,将调用另一个类的构造函数。

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

看起来普通的工厂构造函数也可以创建并返回其他类的实例。这会让重定向工厂变得不必要。然而,重定向工厂构造函数有几个优点:

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

构造函数拆分 (Constructor tear-offs)
Dart 允许你将构造函数作为参数传递而无需调用它。这被称为“拆分”(因为你可以“撕下”括号),它作为一个闭包,使用相同的参数调用构造函数。

如果拆分与方法接受的签名和返回类型相同,你可以将其用作参数或变量。

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

使用拆分:

推荐做法:

dart
// 对命名构造函数使用拆分:
var strings = charCodes.map(String.fromCharCode);

// 对匿名构造函数使用拆分:
var buffers = charCodes.map(StringBuffer.new);

不推荐使用 lambda:

dart
// 不要使用 lambda 来实现命名构造函数:
var strings = charCodes.map((code) => String.fromCharCode(code));

// 不要使用 lambda 来实现匿名构造函数:
var buffers = charCodes.map((code) => StringBuffer(code));

更多讨论,请观看这个关于拆分的 Decoding Flutter 视频。

实例变量初始化 (Instance variable initialization)

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

在声明时初始化实例变量 (Initialize instance variables in the declaration)

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

dart
class PointA {
  double x = 1.0;
  double y = 2.0;

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

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

使用初始化形式参数 (Use initializing formal parameters)

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

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

当存在命名冲突时,使用 this.;否则,按照 Dart 风格可以省略 this.。但有一个例外:在生成式构造函数中,你必须用 this 前缀初始化形式参数的名称。

如本指南前面所述,某些构造函数及构造函数的某些部分不能访问 this。这些情况包括:

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

初始化形式参数还允许你初始化非空(non-nullable)或 final 的实例变量。这两种类型的变量都需要初始化或提供默认值。

dart
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]);
}

私有字段不能用作命名的初始化形式参数 (Private fields can't be used as named initializing formals)。

dart
class PointB {
// ...

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

// ...
}

这种方式也适用于命名变量 (This also works with named variables)。

dart
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 的,并且仅在初始化变量的作用域内有效。

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

构造函数参数可以被设置为可空(nullable)并且不被初始化。

dart
class PointD {
  double? x; // 如果在构造函数中未设置,则为 null
  double? y; // 如果在构造函数中未设置,则为 null

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

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

使用初始化列表 (Use an initializer list)

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

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

警告 (Warning)
初始化列表的右侧不能访问 this

为了在开发过程中验证输入,可以在初始化列表中使用 assert

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

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

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

构造函数继承 (Constructor inheritance)

子类(或称为派生类)不会从它们的父类(或直接父类)继承构造函数。如果一个类没有声明构造函数,它只能使用默认构造函数。

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

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

非默认父类构造函数 (Non-default superclass constructors)

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

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

如果父类缺少无参、未命名的构造函数,则需要调用父类中的某一个构造函数。在构造函数体(如果有的话)之前,使用冒号 (:) 指定父类构造函数。

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

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

dart
class Employee extends Person {
  Employee() : super.fromJson(fetchDefaultData());
  // ···
}

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

超级参数 (Super parameters)

为了避免将每个参数传递到构造函数的 super 调用中,可以使用超级初始化参数(super-initializer parameters)将参数转发给指定的或默认的父类构造函数。你不能在重定向构造函数中使用此功能。超级初始化参数在语法和语义上类似于初始化形式参数。

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

如果 super 构造函数调用包含位置参数,则超级初始化参数不能是位置参数。

dart
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);
}

为了进一步说明,考虑以下示例。

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

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

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

dart
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);
}