Constructors
构造函数是创建类实例的特殊函数。
Dart 实现了多种类型的构造函数。除了默认构造函数,这些函数的名称都与其所属的类名相同。
生成式构造函数 (Generative constructors)
创建新实例并初始化实例变量。
默认构造函数 (Default constructors)
当没有指定构造函数时,用于创建新实例。它不接受参数,也没有名称。
命名构造函数 (Named constructors)
阐明构造函数的用途,或允许为同一个类创建多个构造函数。
常量构造函数 (Constant constructors)
将实例创建为编译时常量。
工厂构造函数 (Factory constructors)
创建子类型的新实例或从缓存中返回现有实例。
重定向构造函数 (Redirecting constructor)
将调用转发给同一类中的另一个构造函数。
构造函数的类型
生成式构造函数
要实例化一个类,请使用生成式构造函数。
1 | class Point { |
默认构造函数
如果你没有声明构造函数,Dart 会使用默认构造函数。默认构造函数是一个没有参数、没有名称的生成式构造函数。
命名构造函数
使用命名构造函数可以为一个类实现多个构造函数,或为构造函数提供更清晰的语义:
1 | const double xOrigin = 0; |
子类不会继承父类的命名构造函数。要在子类中创建一个在父类中定义的同名构造函数,必须在子类中实现该构造函数。
常量构造函数
如果你的类创建的对象是不可变的,可以将这些对象设为编译时常量。为此,需要定义一个 const
构造函数,并确保所有实例变量都是 final
的。
1 | class ImmutablePoint { |
常量构造函数并不总是创建常量。它们也可能在非 const
上下文中被调用。要了解更多信息,请参阅关于使用构造函数的部分。
重定向构造函数
一个构造函数可以重定向到同一个类中的另一个构造函数。重定向构造函数没有函数体,在冒号 (:
) 之后使用 this
关键字而不是类名。
1 | class Point { |
工厂构造函数
当实现一个构造函数时遇到以下两种情况之一,请使用 factory
关键字:
- 构造函数并不总是创建其类的新实例。虽然工厂构造函数不能返回
null
,但它可能返回:- 一个从缓存中获取的现有实例,而不是创建一个新实例。
- 一个子类型的新实例。
- 在构造实例之前需要执行一些复杂的逻辑。这可能包括检查参数或执行任何无法在初始化列表中处理的逻辑。
提示
你也可以使用late final
(请谨慎使用!) 来处理final
变量的延迟初始化。
以下示例包含两个工厂构造函数。
Logger
工厂构造函数从缓存中返回对象。Logger.fromJson
工厂构造函数从一个 JSON 对象初始化一个final
变量。
1 | class Logger { |
警告
工厂构造函数无法访问this
。
像使用任何其他构造函数一样使用工厂构造函数:
1 | var logger = Logger('UI'); |
重定向工厂构造函数
重定向工厂构造函数指定了对另一个类构造函数的调用,每当有人调用该重定向构造函数时,都会使用这个指定的构造函数。
1 | factory Listenable.merge(List<Listenable> listenables) = _MergingListenable |
看起来普通的工厂构造函数似乎也能创建并返回其他类的实例,这使得重定向工厂变得不必要。但重定向工厂有几个优点:
- 一个抽象类可以提供一个常量构造函数,该构造函数使用另一个类的常量构造函数。
- 重定向工厂构造函数避免了转发器(forwarders)需要重复形参及其默认值。
构造函数引用 (Tear-offs)
Dart 允许你在不调用的情况下,将构造函数作为参数传递。这种方式称为“引用(Tear-off)”(就像把括号撕掉一样),它就像一个闭包,可以用相同的参数调用该构造函数。
如果这个构造函数引用的签名和返回类型与方法接受的参数相匹配,你就可以将该引用作为参数或变量使用。
引用与 lambda 或匿名函数不同。Lambda 是对构造函数的包装,而引用本身就是构造函数。
使用引用 (Tear-Offs)
推荐
1 | // 使用命名构造函数的引用: |
不要使用 Lambda
不推荐
1 | // 不要为命名构造函数使用 lambda: |
更多讨论,请观看关于 tear-offs 的 Decoding Flutter 视频。
播放视频:Dart Tear-offs | Decoding Flutter
实例变量的初始化
Dart 可以通过三种方式初始化变量。
在声明中初始化实例变量
在声明实例变量时就进行初始化。
1 | class PointA { |
使用初始化形参
为了简化将构造函数参数赋值给实例变量这一常见模式,Dart 提供了初始化形参。
在构造函数声明中,包含 this.<propertyName>
并省略函数体。this
关键字指向当前实例。
当存在名称冲突时,使用 this
。否则,Dart 风格建议省略 this
。一个例外是生成式构造函数,你必须在初始化形参名称前加上 this
前缀。
正如本指南前面提到的,某些构造函数和构造函数的某些部分无法访问 this
。这些包括:
- 工厂构造函数
- 初始化列表的右侧
- 父类构造函数的参数
初始化形参也允许你初始化非空或 final
的实例变量。这两种类型的变量都需要初始化或一个默认值。
1 | class PointB { |
私有字段不能用作命名的初始化形参。
1 | class PointB { |
这也适用于命名变量。
1 | class PointC { |
所有通过初始化形参引入的变量都是 final
的,并且只在被初始化的变量的作用域内有效。
要执行无法在初始化列表中表达的逻辑,可以创建一个工厂构造函数或静态方法来处理该逻辑,然后将计算出的值传递给一个普通的构造函数。
构造函数参数可以设置为可空类型,并且不进行初始化。
1 | class PointD { |
使用初始化列表
在构造函数体运行之前,你可以初始化实例变量。用逗号分隔多个初始化表达式。
1 | // 初始化列表在构造函数体运行前 |
警告
初始化列表的右侧无法访问this
。
要在开发期间验证输入,可以在初始化列表中使用 assert
。
1 | Point.withAssert(this.x, this.y) : assert(x >= 0) { |
初始化列表有助于设置 final
字段。
以下示例在初始化列表中初始化了三个 final
字段。要执行代码,请点击“运行”。
构造函数的继承
子类不会继承其父类(即直接父类)的构造函数。如果一个类没有声明构造函数,它只能使用默认构造函数。
一个类可以继承父类的参数。这些被称为 **父类参数 (super parameters)**。
构造函数的运作方式与调用一连串静态方法有些相似。每个子类都可以调用其父类的构造函数来初始化一个实例,就像子类可以调用父类的静态方法一样。这个过程不会“继承”构造函数的函数体或签名。
非默认的父类构造函数
Dart 按以下顺序执行构造函数:
- 初始化列表
- 父类的未命名、无参数的构造函数
- 主类的无参数构造函数
如果父类没有未命名、无参数的构造函数,则必须调用父类中的一个构造函数。在构造函数体(如果有)之前,用冒号 (:
) 指定父类构造函数。
在下面的例子中,Employee
类的构造函数调用了其父类 Person
的命名构造函数。要执行以下代码,请点击“运行”。
由于 Dart 在调用父类构造函数之前会先计算其参数,因此参数可以是一个表达式,比如一个函数调用。
1 | class Employee extends Person { |
警告
传递给父类构造函数的参数无法访问this
。例如,参数可以调用静态方法,但不能调用实例方法。
父类参数 (Super parameters)
为了避免将每个参数都传递到构造函数的 super
调用中,可以使用父类初始化形参将参数转发给指定的或默认的父类构造函数。此功能不能与重定向构造函数一起使用。父类初始化形参的语法和语义与初始化形参类似。
版本说明
使用父类初始化形参需要 Dart 语言版本至少为 2.17。如果你使用的是更早的语言版本,则必须手动传入所有父类构造函数的参数。
如果父类构造函数的调用包含位置参数,则父类初始化形参不能是位置参数。
1 | class Vector2d { |
为了进一步说明,请看以下示例。
1 | // 如果你用任何位置参数调用了父类构造函数 (`super(0)`), |
这个命名构造函数试图设置 x
值两次:一次在父类构造函数中,一次作为位置父类参数。由于两者都指向 x
这个位置参数,因此会导致错误。
当父类构造函数有命名参数时,你可以将它们分散在命名父类参数(下一个例子中的 super.y
)和传递给父类构造函数调用的命名参数(super.named(x: 0)
)之间。
1 | class Vector2d { |