協助使用者在 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)以及一組實例變數。這能大幅減少樣板程式碼(boilerplate)並提升程式碼可讀性。
主要優勢
- 將欄位宣告、參數宣告與初始化整合為單一宣告,稱為「宣告式參數宣告(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`。特別的是,我們可以在主建構子內文部分的初始化列表中使用名稱 `x`。'y' 僅為參數,因為它既沒有 `final` 也沒有 `var` 關鍵字,但 `y` 會透過 `this :` 初始化列表傳遞給父類別建構子。
class C(final int x, int y) extends Base {
this : super(y);
}
宣告式參數與初始化參數是達成相同目標的兩種方式:宣告一個包含實例欄位並在建構子中進行設定的類別。一般參數的相異之處在於其數值不會自動路由(routed)至實例欄位。
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)
擴充型別必須使用主建構子。
- 標頭中的單一參數即為表現欄位(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 遮蔽(Shadowing)
在主初始化器作用域內,主建構子參數會遮蔽同名的類別成員(欄位):
- 在非 late 初始化器中:
int y = x其中的x是指參數x。 - 在
late初始化器中:late int y = x其中的x是指欄位x(若存在),因為此時參數x已超出作用域。
3.4 生成建構子限制
為了確保主建構子(及相關的初始化器作用域)總是會執行:
- 宣告了主建構子的類別、mixin class 或 enum 不能宣告任何其他非重定向生成建構子(擴充型別除外)。
- 在內文中宣告的所有其他生成建構子必須(直接或間接)重定向至主建構子。
3.5 參數變更錯誤(Parameter Mutation Errors)
主建構子參數在初始化階段是不可賦值的(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); } -
將欄位移動至標頭(Header):
將欄位放置於標頭並加上final或var修飾詞;若內文為空,請在末尾加上分號(;)。name與age欄位現在於主建構子中分別改寫為宣告式參數final String name與final int age。// 遷移後 class User(final String name, final int age); -
處理自訂初始化器與斷言(Assertions):
若有初始化列表或 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 中省略類別名稱
}






