帮助用户在 Dart 中编写语法与语义正确的主构造函数(Primary Constructors),并迁移或使用全新的构造函数语法、空体分号语法、类体内初始化列表语法以及简写/精简构造函数语法。
Dart 主构造函数与新构造函数语法 Skill
当需要帮助用户编写、重构或调试使用了 Dart 主构造函数(Primary Constructors) 特性的代码时,请使用此 Skill。
Dart 版本要求
- Dart 3.13 及以上:默认启用主构造函数。
- Dart 3.12:提供该特性但属于实验性功能。用户必须通过命令行参数
--enable-experiment=primary-constructors或在analysis_options.yaml中显式开启实验标志primary-constructors:
analyzer:
enable-experiment:
- primary-constructors
- Dart 3.11 及更早版本:不支持主构造函数。
1. 概述
主构造函数允许开发者直接在类头(class header)中声明非重定向生成式构造函数(non-redirecting generative constructor)以及一组实例变量。这极大减少了样板代码,显著提升了代码可读性。
核心优势
- 将字段声明、参数声明与初始化合并为单一声明,称为“声明式参数声明”(declaring parameter declaration)。
- 允许在非 late 字段初始化器中安全地引用构造函数参数(主初始化器作用域 / Primary Initializer Scope)。
- 允许使用分号(
;)精简表示空的声明体。 - 为类体内构造函数引入了简写/精简语法(abbreviated concise syntax)。
2. 语法参考
2.1 基础类头语法
要声明主构造函数,请将参数列表紧跟在类型名称(以及可选的类型参数)之后:
// 声明字段 x 和 y,以及生成式构造函数 Point(this.x, this.y)
class Point(var int x, var int y);
// 声明 final 字段
class PointFinal(final int x, final int y);
2.2 声明式参数、初始化参数与普通参数
主构造函数的参数列表明确区分以下三种参数类型:
- 声明式参数(Declaring Parameters):使用
var或final修饰符标注(如final int x)。它们会在类中隐式创建一个对应的实例字段。 - 初始化参数(Initializing Parameters):使用
this.或super.前缀标注(如this.x或super.x)。它们分别用于初始化已有的字段或父类构造函数参数。 - 普通参数(Regular Parameters):声明时不带任何修饰符(如
int y)。它们不会变成字段,仅在初始化期间可用(例如在字段初始化器中,或者类体内的this :初始化列表中)。
// `x` 是字段也是参数,因为它带有 `final` 关键字。特别地,我们可以在主构造函数的类体内 `this :` 初始化列表中使用 `x` 这个名称。'y' 仅仅是一个参数,因为它既没有 `final` 也没有 `var` 关键字,但 `y` 通过 `this :` 初始化列表传递给了父类构造函数。
class C(final int x, int y) extends Base {
this : super(y);
}
声明式参数和初始化参数是实现相同目标的两种方式:声明一个包含实例字段且字段值在构造函数中设置的类。普通参数的不同之处在于,它们的值不会自动路由并赋值给实例字段。
2.3 常量主构造函数
若要将主构造函数声明为 const,请在声明头的类/类型名称前加上 const 关键字:
class const Point(final int x, final int y);
extension type const Ext(int x);
enum const MyEnum(final int x) {
entry(1);
}
2.4 Extension Types
Extension types 必须使用主构造函数。
- 类头中的单个参数即为表示字段(representation field)。
- 表示变量不能使用
var修饰符(使用var会触发representation_field_modifier错误)。 - 表示变量可以选填
final修饰符。如果不写final则会自动推导;即参数在声明时指定是否显式写明final。
2.5 空体分号简写 (;)
当类(class)、mixin class、mixin、extension 或 extension type 的主体为空时,可以用分号(;)替换大括号({}):
class C(int x);
mixin class MC;
extension type ET(int x);
mixin M;
extension Ext on C;
2.6 主构造函数的类体内部分 (this ...)
如果主构造函数需要断言(assertions)或自定义字段初始化,可以在类体内使用 this : 语法进行声明:
class Point(var int x, var int y) {
// 类体内的初始化列表
this : assert(x >= 0), y = y * 2;
}
你也可以使用 this {...} 语法编写构造函数体。
2.7 简写/精简构造函数语法
对于在类体内声明的构造函数,可以省略类名,改用 new 或 factory 关键字替代:
| 传统语法 | 简写/精简语法 |
|---|---|
MyClass() {} |
new() {} |
MyClass.name() {} |
new name() {} |
const MyClass(); |
const new(); |
const MyClass.name(); |
const new name(); |
factory MyClass() => ... |
factory() => ... |
factory MyClass.name() => ... |
factory name() => ... |
3. 语义与作用域规则
3.1 主初始化器作用域
声明主构造函数时,其形参会被引入到**主初始化器作用域(Primary Initializer Scope)**中。该作用域是类体内非 late 字段初始化器以及主构造函数初始化列表(this : 之后)的当前作用域。
这使得非 late 字段在声明时可以直接引用构造函数参数:
class DeltaPoint(final int x, int delta) {
// 此处 'x' 与 'delta' 均在作用域内
final int y = x + delta;
}
3.2 Late 实例变量限制
主初始化器作用域在 late 实例变量初始化器中不生效。
- 由于
late变量可能会在构造过程完成后才求值,因此它们的初始化器无法安全地访问构造函数参数。 - 尝试在
late字段初始化器中访问主构造函数参数会导致编译期错误。
3.3 作用域遮蔽
主构造函数参数在主初始化器作用域内会遮蔽同名的类成员(字段):
- 在非 late 初始化器中:
int y = x中的x指的是参数x。 - 在
late初始化器中:late int y = x中的x指的是字段x(如果存在),因为此时参数x已超出作用域。
3.4 生成式构造函数限制
为保证主构造函数(及关联的初始化器作用域)总是会被执行:
- 带有主构造函数的类、mixin class 或 enum 声明不能再声明任何其他非重定向生成式构造函数(extension types 除外)。
- 类体内声明的所有其他生成式构造函数必须直接或间接重定向至主构造函数。
3.5 参数修改错误
主构造函数参数在初始化阶段是不可赋值(non-assignable)的。
- 在字段初始化器或
this :初始化列表内对参数进行任何赋值操作(例如p = value、p++)都会引发编译期错误。
3.6 二次初始化错误
对字段进行二次初始化(例如在字段声明/初始化器中初始化了一次,又在 this : 初始化列表中或作为初始化形参再次初始化)会导致编译期错误。
4. 诊断与排错
大多数错误与 lint 警告都支持快速修复(quick-fix),运行 dart fix 即可自动修复。对于其他常见错误,请参考下表进行排查解决:
| 错误 / Lint 代码 | 常见原因 | 解决方案 |
|---|---|---|
| Invalid Late Access | 在 late 字段初始化器中引用了主构造函数参数。 |
将该字段改为非 late,或通过另一个非 late 字段进行值传递。 |
fieldInitializedInInitializerAndDeclaration |
同时在声明处和 this : 列表中初始化了同一个变量。 |
移除其中一处初始化。 |
nonRedirectingGenerativeConstructorWithPrimary |
在类体内声明了生成式构造函数,但未重定向至主构造函数。 | 修改类体内的构造函数使其重定向(例如 this(...)),或者直接移除该类体内构造函数。 |
5. 逐步重构工作流
工作流 5.1:将现有类迁移至主构造函数
请按照以下步骤将冗长的传统类迁移为全新的主构造函数语法:
-
识别候选字段与构造函数:
定位生成式构造函数及其初始化的字段。在本例中,即name和age字段。// 迁移前 class User { final String name; final int age; User(this.name, this.age); } -
将字段移至类头:
将字段放置在类头中,并加上final或var修饰符;若类体为空则追加分号(;)。此时name和age字段在主构造函数中分别写作声明式参数final String name与final int age。// 迁移后 class User(final String name, final int age); -
处理自定义初始化器与断言:
如果原构造函数包含初始化列表或 assert 断言块,将其移动到类体内的this块中:// 迁移前 class Point { final int x; final int y; Point(this.x, this.y) : assert(x >= 0); } // 迁移后 class Point(final int x, final int y) { this : assert(x >= 0); } -
利用主初始化器作用域进行计算:
如果某个字段的值是通过参数计算得出的,可以将其声明在类体内并直接使用参数赋值:// 迁移前 class Rect { final double width; final double height; final double area; Rect(this.width, this.height) : area = width * height; } // 迁移后 class Rect(final double width, final double height) { // 此处 'width' 与 'height' 均在作用域内 final double area = width * height; } -
将类体内构造函数转换为重定向构造函数:
确保类体内声明的所有生成式构造函数都重定向至主构造函数:// 迁移前 class Point { final int x; final int y; Point(this.x, this.y); Point.zero() : x = 0, y = 0; } // 迁移后 class Point(final int x, final int y) { new zero() : this(0, 0); // 重定向至主构造函数 }
工作流 5.2:应用简写(精简)类体内构造函数
当用户希望将构造函数保留在类体内,但又想精简冗长语法时,可以推荐使用简写构造函数语法:
// 简写前
class DatabaseService {
final String url;
DatabaseService(this.url);
DatabaseService.local() : url = 'localhost';
factory DatabaseService.create() => DatabaseService('default');
}
// 简写后
class DatabaseService {
final String url;
new(this.url); // 省略类名,直接使用 'new'
new local() : url = 'localhost'; // 命名构造函数使用 'new local'
factory create() => DatabaseService('default'); // factory 构造函数省略类名
}






