RainVisitor Blog

RainVisitor

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://dart.dev/blog/bringing-primary-constructors-to-dart

Illustration of Dash with blueprint drawing of primary constructor syntax.
將主要建構子引入 Dart

(AI 披露:這篇文章的每個句子都是我親自撰寫的 — 包括破折號。)

我最喜歡的 Dart 3.13 功能是主要建構子。在語言團隊設計出我們認為穩固的設計之前,這花費了大量時間和迭代。由於你們中的許多人一直耐心等待這個功能,我想值得寫一篇關於我們為將這個大的語法變更引入 Dart 所面臨的一些挑戰。

關於語法糖

使用者多年來一直要求類似主要建構子的功能。這是一個備受期待的功能,這有點奇怪,如果你仔細思考的話。主要建構子並不能讓你做任何你已經無法在 Dart 中做的事情。它們只是一種不同——希望更好!——的語法,用於表達你已經可以表達的內容。

在 1960 年代,Peter Landin 創造了「語法糖衣」(syntactic sugaring)這個術語,指的是在更基礎但不愉快的語言之上添加一些文本上的美觀。今天,我們傾向於將這個術語更多地用作名詞,將這些功能稱為「語法糖」。

每個程式語言的宿敵都是複雜性。即使是最小的功能也必須經過設計、規範、實作、測試和文件。成本是巨大的。我將語言中的複雜性視為飛機的重量。一定量的複雜性對於事物運作是必要的,但你必須小心不要不必要地增加重量,否則整個裝置就有可能無法起飛。

從這個角度來看,語法糖似乎是個壞主意。它是額外的複雜性,沒有額外的功用。更糟糕的是,一旦我們添加它,我們也將複雜性轉嫁給我們的使用者。現在他們每次嘗試表達某種內容時,都必須選擇使用哪種語法。

這種功能什麼時候是個好主意呢?(我承認我感覺有必要證明這一點,因為我過去幾年的大部分工作都在為 Dart 添加這類功能。)我認為語法糖可以透過幾種方式發揮作用:

新方法就是簡單好用

儘管我們的語氣有些機械化,並且喜歡 EBNF,但我們語言設計師是人,也會犯錯。此外,我們一直在學習,我們服務的生態系統不斷發現製作軟體的新方法,並且使用者期望會隨著時間推移而改變。

Dart 最初設計時,必須使用明確的 new 關鍵字來呼叫建構子。這是為了讓來自 C++、Java、JavaScript 和其他語言的使用者感到熟悉。其意圖是在程式碼中更清楚地標示何時呼叫會分配一個新物件。隨著垃圾收集器變得更好,使用者也更習慣於自動記憶體管理,大多數使用者發現 new 造成的噪音多於信號。

(老實說,Dart 一直透過支援工廠建構子來破壞這個信號。即使你用 new 調用工廠建構子,它也可以返回之前創建的物件。)

在 Dart 2.0 中,我們發布了一項語言變更,允許您在呼叫建構子時省略 new 關鍵字(以及許多地方的 const)。我們仍然支援舊的語法,所以這個語言變更本質上是語法糖,但我們真正保留舊語法只是為了向後相容性。

我們總是希望您使用新的簡短語法。我們提供了工具來自動移除不必要的 new 關鍵字,並且有 一個 lint 提醒您何時忘記。舊的語法實際上已經被棄用,隨著時間的推移,您會越來越少看到它。如果您是 Dart 的新手,您甚至可能沒有意識到我們支援在建構子呼叫中使用 new

這意味著支援有 new 和沒有 new 的建構子呼叫的複雜性很低。現有使用者學習新語法需要一個過渡成本。但新使用者大多只會學習新方法,而不會遇到舊方法。在兩種語法之間選擇時幾乎沒有認知負擔,因為您只需始終使用新語法(如果沒有,我們的工具會輕輕地提醒您)。

除非有時光機可以回到過去並第一次就做對,否則這是我們為修復語言錯誤所能做的次優選擇。

對於常見的使用場景,語法可以好很多

在其公開存在的最初幾年,Dart 不支援 enum 宣告。該語言不允許你這樣寫:

1
enum Color { red, blue, yellow }

相反,您必須這樣寫:

1
2
3
4
5
6
7
8
9
10
class Color {
static const Color red = Color._(0, 'red');
static const Color blue = Color._(1, 'blue');
static const Color yellow = Color._(2, 'yellow');

const Color._(this.index, this.name);

final int index;
final String name;
}

老 Java 程式設計師會記得這就是 Josh Bloch 的「型別安全列舉模式」。在底層,這個更冗長的類別宣告幾乎與 Dart 中今天的列舉宣告做完全相同的事情。Dart 列舉宣告幾乎完全是糖。(我說「幾乎」是因為列舉宣告在 switch 中提供 窮舉檢查)。

然而,從這兩個例子你可以看出,列舉宣告是非常棒的語法糖。一個簡單的列舉宣告會展開成許多 Dart 程式碼。現在,如果幾乎沒有人撰寫列舉型別,那麼為此使用場景添加語法來優化可能仍然不值得。但在一個優先考慮型別安全和資料驗證的語言中,列舉非常常見。僅 Flutter 框架就定義了數十個。

相對少量的語法糖有時可以使大量使用者程式碼更短更簡單。

語法可以使意圖更清晰

上一節聽起來像是簡潔就是重點。我想在一個我們越來越多地按每個 token 的成本付費給 AI 代理來讀寫程式碼的世界中,這有直接的財務誘因。但這不僅僅是字元計數。考慮一下:

1
2
3
4
5
6
7
8
9
10
class Color {
static const Color red = Color('red', 0xff0000);
static const Color blue = Color('blue', 0x0000ff);
static const Color yellow = Color('yellow', 0xff00ff);

const Color(this.name, this.rgb);

final String name;
final int rgb;
}

這是一個列舉型別嗎?我的意思是「列舉型別」:使用這個類別的人是否應該假設他們唯一需要擔心的 Color 實例是 redblueyellow

請注意,建構子公開的,因此其他函式庫可以自由調用建構子並創建其他顏色。這個類別的意圖是一個封閉的顏色列表,還是一個帶有一些預定義值的開放工廠?

閱讀程式碼,我們不知道。程式碼是大量定義型別和一些常量的機制。它看起來像你想要列舉時會寫的程式碼,但機制並沒有揭示意圖。程式碼告訴編譯器程式碼的含義,但它沒有告訴讀者如何使用它。

如果我們將其更改為列舉宣告,那麼它是封閉值集合的策略就會變得顯而易見。(而且,現在 Dart 有真正的列舉,選擇將此程式碼更改為列舉宣告可能表示它不是封閉集。)

對我來說,這是添加語法糖的一個令人信服的理由。程式碼是作為語法編寫和執行的,但每個處理程式碼的使用者關心的是它意味著什麼—它的語義。要正確維護程式碼,我們需要理解其意圖和策略。在 AI 常常比我們有時間仔細審查程式碼更快地生成程式碼的世界中,這一點越來越重要。

即使可以透過將多個現有語言功能的機制拼湊起來讓編譯器執行您想要的操作,擁有能產生相同行為的語法糖也可能值得,因為更好的語法將該行為提升到更高的抽象層次,其中預期的語義更為明顯。這可以減少理解程式碼所需的認知工作,儘管整個語言變得更複雜。

為什麼選擇主要建構子

好的,我應該討論主要建構子,而不是列舉和 new 關鍵字。(儘管 — 預示!— 我也會討論 new。)多年來,Dart 語言儲存庫上 #1 的開放議題 一直是資料類別的功能請求。如果您不知道,資料類別是 Kotlin 的一個功能,它允許您定義一個帶有一些欄位的類別,編譯器會免費為您提供相等性、雜湊碼和其他一些東西。

如果你仔細閱讀該議題上的數百條評論,你會發現大多數使用者對值語義部分 — 等式和雜湊碼 — 不那麼感興趣。他們主要感興趣的是以更簡單的方式定義具有建構子並儲存一些狀態的類別。

該功能實際上來自 Kotlin 的一個不同的、更基礎的功能:主要建構子。我相信 Kotlin 從 Scala 獲得了這個想法。從那以後,C#Java 也引入了他們自己對這個概念的理解。

(資料類別的值語義部分也很有用。我們正在單獨探索這個問題。)

主要建構子有兩個關鍵部分:

  • 您可以透過將參數列表直接寫入類別標頭來定義建構子。這樣可以避免重複寫關鍵字或重複類別名稱來宣告建構子。在只包含一些狀態的非常簡單的類別中,它還避免了類別主體和建構子參數列表的兩級嵌套和縮進。
  • 在該參數列表內部,您可以指出某些參數應宣告相應的實例欄位,這些欄位會從該參數自動初始化。

如果沒有主要建構子或任何其他語法糖,我們必須在 Dart 中執行類似以下操作:

1
2
3
4
5
6
class Point {
final int x;
final int y;

Point(int x, int y) : x = x, y = y;
}

在這個例子中,我們必須寫兩次類別名稱。對於每一點狀態,我們寫了它的型別兩次,它的名稱次。在這個例子中,它並不太糟糕,因為只有兩個欄位,而且名稱都很短。一旦你開始處理具有長名稱和大量狀態的複雜領域特定內容,它就會變得醜陋。

這不是一個新問題,Dart 長期以來都有一點語法糖,稱為「初始化形式參數」,以提供幫助:

1
2
3
4
5
6
class Point {
final int x;
final int y;

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

在建構子參數上使用 this. 意味著您只需寫入每個欄位的類型一次,名稱兩次。更好!但您仍然必須寫入類別名稱兩次,每個欄位的名稱兩次。初始化形參很好,但使用者仍然告訴我們它們不夠用。

關於借鑒其他語言的功能

那麼,一些從其他語言轉向 Dart 的使用者告訴我們他們缺少某個功能。我們該如何處理這類回饋呢?

我個人喜歡從其他語言借鑒功能。那些語言的創造者已經投入了大量工作來設計和驗證該功能。我們可以從他們那裡學到很多東西,而那些其他語言的存在證明了該功能在概念上是連貫且易於實作的。

從其他語言汲取靈感也可以讓我們的語言更容易學習。除非使用者是完全的程式設計新手,否則他們不會從頭開始學習 Dart。他們帶著從其他語言學到的一切來到我們這裡。他們需要學習的,是他們所知與 Dart 所包含內容之間的差異。當我們從其他語言借用語法和語義時,我們就縮小了這種差異的規模,並降低了學習 Dart 的努力。

這種哲學一直是 Dart 成功的關鍵。從微小的分號到類別,Dart 的設計都旨在讓來自 JavaScript、Java 和 C# 等其他主流語言的使用者感到熟悉且易於學習。

同時,好的語言設計是情境化且全面的。「一雙好鞋是什麼?」在北極苔原和夏威夷海灘上的答案會非常不同。一個在 Rust 中運作良好的語言功能,可能無法優雅地融入 Dart,因為 Dart 有其獨特的語法、語義、歷史、使用者基礎和生態系統。

我不希望 Dart 感覺像是一個由其他語言中撕裂下來的身體部位拼湊而成的科學怪人。因此,當 Dart 語言團隊研究其他語言的功能時,我們同時會研究該功能如何在該語言的上下文中解決問題,以及該上下文與 Dart 自己的匹配程度。

將主要建構子引入 Dart

我們知道使用者想要一種更好的語法來定義一個從建構子參數初始化某些欄位的類別。透過主要建構子,您編寫建構子,編譯器則綜合欄位。語言也可以反其道而行之。您編寫欄位宣告,編譯器則免費為您提供建構子。Swift 使用成員初始化器來實現。

任何時候你的語言從一個語法片段中衍生出兩個宣告時,都會面臨一個挑戰:這個語法需要處理所有你可能配置這兩個宣告的各種方式。在我們這裡的案例中,實例欄位可能是 final 或不是。它可能帶有 override 等元資料或文件註釋。建構子可以是具名或匿名,const 或不是。建構子參數可以是位置參數或具名參數,可選或必選。如果它是可選的,它可能需要指定一個預設值。

我們花了一些時間研究 從欄位宣告推斷建構子,但最終決定參數是使用者手動編寫更有用的宣告。由於建構子通常是公共 API,因此完全控制簽名很重要:建構子的名稱和 const 屬性、哪些參數是具名或位置參數、位置參數的順序以及它們的預設值。

為了從建構子參數推斷實例欄位,使用者需要提供的唯一缺少的部分是該欄位是否應該是 final。允許在參數前加上 finalvar 來控制這一點是相當自然的。兩者皆無修飾符則表示該參數根本不宣告實例欄位。這類似於 Scala 和 Kotlin 對 valvar 的處理方式。Dart 的結果如下:

1
2
3
4
class Point(
final int x,
final int y,
);

(由於主要建構子使空類別主體更常見,我們現在也允許您使用 ; 而不是 {} 來表示空類別主體。)

關於語法懸崖

這看起來很不錯,但是如果主要建構子也需要一個主體或初始化列表怎麼辦?一種選擇是簡單地說:「好吧,在這種情況下,不要使用主要建構子。」語法糖通常會採用用例的一個子集,並為它們提供更簡潔的語法。如果您不屬於該子集,則要求使用者退回到更舊、更複雜的語法是合理的。

在某些情況下,這是正確的選擇。但語言團隊非常注意程式碼會隨著時間演進。假設您正在編寫一個類別。它一開始很簡單,只有幾個從建構子參數初始化的欄位:

1
2
3
4
5
6
class FormatterOptions({
final int indent = 0,
final int pageWidth = 80,
}) {
// ...
}

主要建構子的完美用例。後來你添加了一些欄位和參數。太棒了。沒多久,你就有了一個包含許多宣告欄位的參數的建構子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FormatterOptions(
{final int indent = 0,
final int pageWidth = 80,
final Version? languageVersion,
final TrailingCommas? trailingCommas,
final bool followLinks = false,
final Show show = Show.changed,
final Output output = Output.write,
final Summary summary = Summary.none,
final bool setExitIfChanged = false,
final List<String> experimentFlags = const [],
}) {
// ...
}

然後有一天你決定要在建構子主體中做一些記錄。如果主要建構子不支援主體,那麼你就必須將整個主要建構子轉換成一個內建建構子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FormatterOptions {
final int indent;
final int pageWidth;
final Version? languageVersion;
final TrailingCommas? trailingCommas;
final bool followLinks;
final Show show;
final Output output;
final Summary summary;
final bool setExitIfChanged;
final List<String> experimentFlags;

FormatterOptions(
{this.indent = 0,
this.pageWidth = 80,
this.languageVersion,
this.trailingCommas,
this.followLinks,
this.show = Show.changed,
this.output = Output.write,
this.summary = Summary.none,
this.setExitIfChanged = false,
this.experimentFlags = const [],
}) {
log.write('Created options.');
}

// ...
}

這是可行的。Dart SDK 包含了許多快速修復工具,您只需點擊按鈕即可為您完成這些確切的更改,因此更改程式碼在機制上並不困難。但它仍然是一個很大的文字更改。您只是想添加一行日誌記錄,現在您有 20 行更改需要查看。

在語言團隊中,我們稱之為「語法懸崖」。您想進行一個小的語義變更(這裡,添加一行日誌記錄),但您想表達的內容恰好略超出優化語法所支援的範圍。您從該語法的平穩高原上跌落,降落在下方更冗長的區域。

當這種情況發生時感覺不好。你正在努力自由探索程式的語義空間,但在語法方面,一些微小的意義上的步驟似乎觸手可及。在設計語言功能時,我們花費大量時間討論這些懸崖,並在可能的情況下努力避免它們。

我們希望語言感覺像平坦的地形,小的語義變更只需要同樣小的文本變更。當您進行程式碼審查時,我們希望變更的行反映程式的行為變更,而不是語言語法中沒有意義的橫向移動。

主要建構子主體

Kotlin 的解決方案是允許在類別主體內使用初始化器區塊。在 Dart 中,我們支援類似的功能,但使用 this 關鍵字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FormatterOptions(
final int indent = 0,
final int pageWidth = 80,
final Version? languageVersion,
final TrailingCommas? trailingCommas,
final bool followLinks = false,
final Show show = Show.changed,
final Output output = Output.write,
final Summary summary = Summary.none,
final bool setExitIfChanged = false,
final List<String> experimentFlags = const [],
) {
this() {
log.write('Created options.');
}
}

初始化區塊為您提供了一個空間來填寫主要建構子的主體或初始化列表。它也是為建構子添加文件註釋的自然場所。(如果您將文件註釋放在類別標頭上方,它將應用於整個類別,而不僅僅是主要建構子。)

這些初始化區塊有點像語法糖之上的語法糖。我們不需要它們,但它們有助於防止使用者跌落語法懸崖。您可以從一個主要建構子開始,當您的類別簡單時,語言永遠不會在您的類別演變時迫使您放棄該選擇。

關於語義綁定

現在,即使語言不會強迫您將主要建構子變成內部建構子,可能仍然希望在類別主體內定義一個建構子。類別標頭中可能有很多內容:型別參數、extends 子句、with 子句中的混入,可能還有 implements。建構子參數可能會有文件註解。在那裡可能會變得雜亂無章。

或者,您可能有一個具有多個建構子的類別,其中沒有一個建構子明顯比其他建構子更「主要」。(當您有一個主要建構子時,類別的所有其他非工廠建構子都必須重定向到它。) 或者,也許最基本的建構子是私有的,您認為將私有建構子放在高度可見的類別標頭中看起來很混亂。

由於這些原因以及更多,Dart 仍然支援在類別主體內宣告的建構子。我們不認為主要建構子本質上優於內部建構子,只是不同且更適合某些使用情況。

然而,只有主要建構子才能存取參數上的 varfinal 語法糖,該參數隱式宣告一個實例欄位並從該參數初始化它。這兩個功能——在類別標頭中宣告建構子和引導欄位的建構子參數——是捆綁在一起的。沒有前者就不能使用後者。

一般來說,我們在語言設計上盡力不讓使用者處於這樣一種境地:他們只想要兩種行為中的一種,但語言卻將它們綁定在一起,迫使他們接受兩種。例如,當我們添加類別修飾符時,我們特意添加了 finalsealed。它們非常相似,但 final 允許您防止子類化,而不必選擇啟用窮舉檢查。

決定捆綁什麼是一種平衡的行為。我相信,賦予每種程式語言其特性和適用於某些領域的很大一部分原因是它如何選擇將語義映射到語法。其中一部分是它如何將多種行為掛載到單個文本上。例如,在大多數物件導向語言中,使一個類別成為另一個類別的子類別,也使其成為靜態型別系統中的子型別。這並非絕對必要,正如 C++ 中的私有繼承所示。但對於大多數物件導向語言來說,這種耦合似乎是有意義的。

擁有行為與文字之間嚴格的一對一映射似乎是理想的,但捆綁行為可以讓表達常見模式更容易。想想走進一家漢堡店說「我要一份 2 號餐」比說「一個雙層起司漢堡,加芥末不加番茄醬,中薯,和一杯中杯汽水」容易多少。

當談到將宣告參數與主要建構子結合時,這感覺是一個相對安全的捆綁。宣告參數本身就是語法糖,並且不能讓您表達任何您已經無法在普通類別宣告中表達的內容。如果您出於任何原因不想讓建構子成為主要建構子,您必須放棄宣告參數的語法便利性。但您放棄的只是簡潔性。您仍然可以按照您想要的方式定義您的類別,並具有完全符合您需求的 API 和語義。

能夠在內部建構子中使用宣告參數會很棒,我們也為此努力提出了一個建議。最終,我們認為負面影響太多了。如果在類別主體中的任何隨機建構子都可以隱式宣告其實例欄位在其參數列表中,那麼要找到一個類別儲存的所有狀態並推斷它就變得更加困難。這在主要建構子中不是一個問題,因為主要建構子總是位於類別的頂部。

更短的內部建構子

Dart 現有建構子宣告語法冗長度的另一個來源是必須重複類別名稱。在 Point 這樣的範例中,使用短名稱看起來還不錯,但當您有一個像 AnimatedFractionallySizedBox 這樣的類別名稱時,重複整個 28 個字元的識別碼會佔用大量空間,而這些空間原本可以用於建構子的參數列表。

這個問題我們可以解決。Dart 的建構子語法繼承自 Java 和 C#,而 Java 和 C# 又繼承自 C++。Bjarne Stroustrup 選擇使用類別名稱是為了最大限度地減少新關鍵字的數量,並模仿建構子呼叫的樣子。這是一個可愛的語法,但連他自己也承認「這可能過於聰明了」。

語言語法的一部分與另一部分互相映照,這是一種有用的工具,可以幫助使用者理解程式碼的含義。當兩段程式碼看起來相同時,它會發出一個訊號,表明它們之間可能存在某種關聯。如果宣告看起來像呼叫點,那麼一旦你閱讀了宣告,你就知道如何撰寫程式碼來呼叫它。

至少,這是個想法。但 Dart 並沒有完全貫徹這個原則。如果忽略型別註釋,它對函式和方法宣告還算有效。Getters 使用 get 關鍵字宣告,但調用時卻不使用。運算子使用特殊的 operator 關鍵字宣告,並且右側參數在括號中,儘管你不需要括號來呼叫運算子。即使在函式中,Dart 也使用 {...} 來宣告具名參數,這與你在呼叫點傳遞它們的方式完全不同。

大多數其他物件導向語言不使用類別名稱來定義建構子。Swift、Ruby 和 Objective-C 使用 init()。Python 灑上一些底線並使用 __init__()。JavaScript、TypeScript 和 Kotlin 使用 constructor。PHP 使用 __construct

這使我們得出結論,重複建構子的類別名稱在熟悉度或一致性方面並沒有為我們帶來太多好處。而且使用者必須付出代價。名稱通常很冗長,每個希望將建構子注入類別的人類、模板處理器、程式碼生成器、生成程式碼的 AI 代理或未來的元程式設計功能都需要知道要使用類別名稱。這幾乎就像每個類別都有自己特殊的小上下文關鍵字。

這個問題在我們目前正在開發的另一個功能中尤其突出:靜態擴展成員。Dart 中的擴展允許您將實例成員附加到現有型別,但它們目前不允許您添加靜態成員或建構子。我們也想支援這些,但這會引發一個棘手的邊緣情況。擴展不僅可以定義在類別上,還可以定義在任何靜態型別上,包括 typedef。考慮以下情況:

1
2
3
4
5
6
7
class SomeClass {}

typedef OtherName = SomeClass;

extension on OtherName {
// ...
}

如果你想在這個擴展中添加一個建構子,你該用哪個名稱:OtherName 還是 SomeClass?請記住,從型別系統的角度來看,它們是完全相同的型別。程式中沒有任何地方有名為 OtherName 的類別。typedef 並沒有創建一個新的具名型別,它只是定義了一個可以用來指代其他型別的臨時別名。

我們可以要求你使用 OtherName,因為那是你在擴展宣告頂部寫下的名稱。但這違反了型別可以替換為指代相同型別的 typedef 而不破壞任何東西的原則。通常,用 typedef 替換對某種型別的引用是一個透明的更改。在這裡,而且只有在這裡,typedef 名稱才會變得有意義。

或者我們可以採取另一種方式,說你必須使用 typedef 解析到的底層類別的名稱。但這會破壞 typedef 的封裝。typedef 可能在另一個函式庫中,並引用一個你在你的函式庫中甚至無法存取的私有類別!

這裡的所有複雜性都是我們自己透過使用類別名稱作為「建構子」的魔術識別碼所造成的問題。如果我們只是選擇一個通用的關鍵字,這個問題就會消失。

所以這就是我們所做的。在 Dart 3.13 中,您可以使用 new 代替類別名稱來宣告非工廠建構子,並使用 factory 來宣告工廠建構子。如果您想要一個具名建構子,請將名稱放在 newfactory 關鍵字之後。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Before Dart 3.13:
class LongClassName {
LongClassName(); // Unnamed constructor.
LongClassName.create(); // Named constructor.
}

class AnotherLongClass {
factory AnotherLongClass() { ... } // Factory constructor.
factory AnotherLongClass.create() { ... } // Named factory constructor.
}

// New Dart 3.13 syntax:
class LongClassName {
new(); // Unnamed constructor.
new create(); // Named constructor.
}

class AnotherLongClass {
factory() { ... } // Factory constructor.
factory create() { ... } // Named factory constructor.
}

(重定向建構子的語法類似。)

這屬於我們認為絕對更好的語法糖類別。除非您的類別名稱短於三個字母,否則新語法總是更短。它更規則。除了目前不熟悉(這種感覺會過去),我們認為它總體上更好。

然而,使用 new 來定義建構子確實會導致一個奇怪的組合。如果您也希望該建構子是常量,則需要一個 const 修飾符:

1
2
3
class SomeClass {
const new() { ... }
}

我承認 const new 看起來自相矛盾。對於那些經歷過每個建構子調用都以 newconst 開頭的 Dart 使用者來說,更是如此。在那個世界裡,兩者是直接對立的。但現在你不再寫 new 來調用建構子,所以在我看來,new 關鍵字主要只是意味著「宣告一個建構子」,而 const 總是一個修飾符,意味著「使事物成為常量」。

總結

感謝您與我一起回顧這漫長的設計過程。我們花了這麼長時間仔細思考建構子的語義和語法的每一個細節,為 Dart 3.13 做準備,我還可以再寫五千字來討論它。我們曾考慮將主要建構子參數列表放在類別標頭中的 extendsimplementswith 子句之後。我們就類別主體的每個角落進行了辯論,以決定主要建構子參數應該在哪個範圍內。我們是否應該允許在類別標頭中調用超類?這對 with 子句意味著什麼?如果您有一個名為 factory 的工廠建構子會發生什麼?

建構子是物件導向程式設計的基礎,Dart 已經在它們上面花費了大量的語言複雜性。將主要建構子織入 Dart 需要非常仔細地修補語言結構中的數十處皺褶。我希望結果感覺無縫,但我確信它並不完美。

如果您在新功能中遇到任何感覺奇怪或任意的部分,我希望這篇長文能幫助您更好地理解。如果沒有,請告訴我們。語言總是在發展,我們也總是在努力使其變得更好。同時,我們希望您會發現 Dart 3.13 中的程式碼感覺更清晰、更簡單、閱讀和編寫起來更愉快。

更多來自 Dart

History of JS interop in Dart

Dart 中的 JS 互操作歷史

由於 Dart 3.3 達到了令人興奮的 JavaScript 互操作里程碑,Wasm 的支援剛剛在當前的 Flutter Beta 版中推出。要了解...

Sigmund Cherem Sigmund Cherem
2024年3月28日 · 6 分鐘閱讀
Dart DevTools: Analyzing application performance with the CPU Profiler

Dart DevTools:使用 CPU Profiler 分析應用程式效能

無論您是使用 Dart 編寫命令列工具的後端開發人員,還是使用 Flutter 構建應用程式的 UX 工程師,程式效能對於專案的成功都至關重要。命令列工具應最大限度地減少延遲,應用程式應響應迅速且流暢,沒有丟失的幀。作為開發人員,我們盡力編寫高效能程式碼,但有時不清楚為什麼我們的程式碼效能不如預期。

Ben Konyi Ben Konyi
2023年6月12日 · 13 分鐘閱讀

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/building-multi-agent-dev-teams

架構師、測試人員和程式設計師:建立多代理開發團隊

如何讓在 Antigravity 中運行的多代理開發團隊使用測試驅動開發將 Python 函式庫移植到慣用的 Dart 套件。

當我第一次開始嘗試 AI 編碼助手時,我只使用一個代理來處理所有事情:架構程式碼、編寫單元測試和偵錯堆疊追蹤。雖然這種方法對於小任務運作良好,但對於複雜的軟體工程問題,它很快就會退化。隨著我的對話歷史記錄增長,代理的上下文視窗會被日誌和搜尋結果飽和,導致幻覺、遺漏連結、馬虎的程式碼和錯誤。

幾週前,我偶然發現了一篇關於多代理、TDD 編碼工作流程的部落格文章。我以前聽說過這種方法,但從未嘗試過。由於 Antigravity 的 Agent Hub 包含了多代理設定的工具,我決定對這種技術進行測試。

為了在真正的挑戰中測試它,我著手將流行的 python-statemachine 函式庫移植到靜態類型、無反射的 Dart 套件。儘管 pub.dev 已經有穩定的狀態機選項,但 python-statemachine 是一個絕佳的基準。它嚴重依賴於動態 Python 功能(例如元類和執行時回呼),這些功能必須針對 Dart 強大的靜態類型和 Flutter 對不使用 dart:mirrors 的要求進行徹底重新架構。

這比看起來要複雜,我發誓!

我學到的第一件事是,有效代理團隊的基礎是一組明確定義的角色,這些角色對代理可以做的事情有特定的限制。為了實施這些,我建立了一個總體工作流程技能(tdd-dart-workflow)來建立共用的 TDD 規則和權限,以及四個定義每個代理的角色特定技能:

  • **架構師 (tdd-dart-architect)**:分析原始碼,生成前端的 architecture_blueprint.md (階段 0),並在 specs/ 下編寫有針對性的模組規範 (階段 1)。架構師不能寫入 lib/test/example/
  • **測試人員 (tdd-dart-tester)**:根據架構師的規範 (階段 2a) 在 test/ 下編寫全面的失敗單元測試。測試人員不能查看或寫入 lib/specs/
  • **程式設計師 (tdd-dart-coder)**:在 lib/src/ 下建立編譯骨架並實作函式庫程式碼以使失敗的測試通過 (階段 2b 和 3)。程式設計師不能編輯 test/specs/
  • **協調者 (tdd-dart-coordinator)**:協調者父代理。協調者管理 Git 分支提交,執行 dart analyze && dart test 驗證,並將任務委派給子代理。

透過強制執行嚴格的角色分離,沒有任何單一代理可以同時修改測試斷言和底層程式碼。協調者充當守門員,僅在子代理完成任務時收到簡短的完成報告。這建立了一個「認知防火牆」。除了在工作面試中是一個有趣的詞語之外,這意味著每個子代理的試錯標記歷史記錄會在任務完成後被丟棄。因此,父協調者的上下文視窗保持乾淨、專注,並且沒有標記飽和。

需要澄清的是,我在此處省略了一些潛在問題,例如「如果子代理陷入循環會發生什麼?」和「是否應該設定一個時間截止,在此之後子代理會被終止並重新創建或通知人類?」不過,這是我首次涉足多代理團隊,這些問題我將留到後續的部落格文章中再討論!

為了防止競爭條件和未經證實的程式碼提交,檔案系統寫入權限在工具層級的 tdd-dart-workflow 中受到限制,具體取決於代理的指定角色:

代理角色 允許讀取路徑 允許寫入路徑 禁止操作
協調者 任何地方 任何地方 直接程式碼修改 (委派給程式設計師/測試人員)
架構師 任何地方 specs/, skills/ 不能寫入 lib/test/example/
測試人員 任何地方 test/, example/ 不能寫入 lib/specs/
程式設計師 任何地方 lib/, example/ 不能寫入 test/specs/

在我的版本中,這些限制包含在代理的說明中,因此它們技術上可能會被違反。然而,隨著多代理系統的演進,我預計我們將開始看到以更安全的方式實施限制的方法。

一個挑戰範例

在像 Dart 這樣的編譯語言中操作多代理團隊帶來了一個有趣的挑戰:靜態編譯和為尚不存在的程式碼編寫測試之間的緊張關係。在像 Python 這樣的動態語言中,TDD 以一個拋出運行時錯誤的失敗測試(紅)開始。然而,在靜態語言中,為尚不存在的方法編寫單元測試會導致編譯時失敗。編譯器會中止,阻止測試執行器執行並證明測試本身是有效的。

我的代理團隊在 紅色階段 循環中解決了這個問題。首先,測試人員根據架構師的規範編寫測試。接下來,程式設計師在 lib/src/ 下建立一個編譯骨架,其中包含返回虛擬值或拋出 UnimplementedError 的類和方法存根。一旦骨架滿足編譯器,協調者會運行 dart analyze && dart test 以驗證測試因正確的原因(未實現的邏輯而不是語法或匯入錯誤)而失敗。註釋邊界確保臨時骨架存根與永久測試工具隔離,允許協調者在程式設計師完成綠色階段後自動清除它們:

1
2
3
4
5
6
7
8
9
// TEST UTILITIES - KEEP PERMANENTLY
class MockListener extends Mock implements StateMachineListener {}

// SKELETON STUBS FOR COMPILATION - DELETE ONCE SKELETON IS IMPLEMENTED
class StateMachine<T extends StateModel> {
dynamic get currentState => throw UnimplementedError();
dynamic get currentStateValue => throw UnimplementedError();
void send(String eventId) => throw UnimplementedError();
}

出了什麼問題:摩擦點和飛行中的技能更新

使用代理確實加快了移植函式庫的過程,但它也帶來了自己的挑戰。然而,工作流程中斷的時刻提供了最有價值的工程教訓。在移植過程中,我不得不暫停並更新我的技能檔案(SKILL.md)以解決三個問題:

  • 意外刪除測試工具:一旦程式設計師在 lib/src/ 中實作了真實的函式庫類別,協調器就必須清理測試檔案中留下的臨時編譯存根。它總是將存根與真實的東西混淆,因此需要清晰的註釋標頭才能使真實的實作清晰。
  • 展開運算子 (...) 類型不匹配:Python 和 Dart 處理類型的方式不同,Python 中預設可迭代的東西在 Dart 中不一定如此。我不得不針對需要實作 Iterable 的東西新增一些明確的說明。
  • 「帶有外來口音」的動態類型:再次地,不同的類型系統造成了問題。原始的 Python 函式庫在幾個地方使用了 hasattr 檢查。我不得不改進 tdd-dart-architect/SKILL.md 以強制架構師使用慣用的 Dart 和明確的回呼委派,而不是動態屬性查找。

這些技能改進直接塑造了最終的設計。產生的 Dart 套件 (state_machine) 保留了 python-statemachine 的完整功能集,程式設計師代理實作了流暢的建構器以支援 Dart 的原生展開運算子 (...)。以下是原始 Python 類別定義與代理團隊產生的最終 Dart 狀態機之間的比較:

舊方式:

1
2
3
4
5
6
7
8
9
10
11
12
# python-statemachine syntax (declarative via metaclasses)
from statemachine import StateMachine, State

class TrafficLightMachine(StateMachine):
green = State("Green", initial=True)
yellow = State("Yellow")
red = State("Red")

cycle = green.to(yellow) | yellow.to(red) | red.to(green)

def on_enter_green(self):
print("Entered Green state")

新方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// state_machine Dart syntax (declarative constructor DSL)
import 'package:state_machine/state_machine.dart';

class TrafficLightMachine extends StateMachine<TrafficLightModel> {
final green = State('green', initial: true);
final yellow = State('yellow');
final red = State('red');

late final Event cycle;

TrafficLightMachine(TrafficLightModel model) : super(model: model) {
cycle = event('cycle');

initialize(
states: [green, yellow, red],
transitions: [
...green.to(yellow).on(cycle).transitions,
...yellow.to(red).on(cycle).transitions,
...red.to(green).on(cycle).transitions,
],
);

green.onEnter((EventData data) {
print('Entered Green state');
});
}
}

所有這些的重點更多是為我自己產生一套可重複使用的技能,而不是建立和發布一個新的 Dart 套件。我目前無法監測報告的問題並長期維護這個套件。我無法保證所有建立的程式碼都沒問題(儘管在我測試時它確實有效),而且生態系統中已經有幾個維護良好的套件。然而,希望這能讓您了解在 Antigravity 中運行的多代理系統能為 Dart 和 Flutter 開發人員做些什麼。

如果您想在自己的專案中探索多代理工作流程,請嘗試一下!

  • 下載 Antigravity
  • 造訪 AGY 入門文件,了解如何定義自己的代理和自訂開發人員技能。
  • 查看我在這個專案中建立的實際技能檔案,根據您的需求修改它們,並讓我們知道進展如何!

Flutter 的更多內容

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/speeding-up-generative-ui-with-async-a2ui

使用非同步 A2UI 加速生成式 UI

生成式 UI (GenUI) 正在改變我建構使用者介面的方式。GenUI 允許 AI 代理程式在執行時使用基於 JSON 的訊息動態組合和更新使用者介面,而不是向每個使用者呈現一組靜態畫面。在 Flutter 中,genui 套件讓我可以即時渲染這些 AI 驅動的組件。

閱讀全文 »

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/whats-new-in-flutter-3-47

Flutter 3.47 的新功能

依模組設計:獨立 UI 套件和桌上型 Impeller

What's new in Flutter 3.47
Flutter 3.47 的新功能

Flutter 3.47 登場,帶來了一些令人興奮的更新。

今天,我們很高興地發佈獨立 material_uicupertino_ui 套件的 1.0 版本。這是一個重要的里程碑,它將設計系統從核心 SDK 中分離出來。

閱讀全文 »

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://flutter.dev/blog/flutter-q2-2026-survey

Flutter 2026 年第二季度使用者調查 — 信任、透明度和不斷發展的社群

Flutter 2026 年第二季度使用者調查結果出爐!探索開發者滿意度、信任度、AI 編碼工具採用情況以及 Cupertino widgets 的更新。

閱讀全文 »

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

原文:https://medium.com/dartlang/weve-moved-the-official-dart-blog-has-a-new-home-2b9ea40d2a72

官方 Dart 部落格已遷移至 dart.dev/blog

歡迎來到我們的新家!官方 Dart 部落格已從 Medium 遷移到 dart.dev/blog。未來,您將在此處找到所有語言發佈公告、深入技術文章以及來自 Dart 團隊的工程更新。

閱讀全文 »

【文章內容使用 Gemini 2.5 Flash 自動翻譯產生】

與 Rody Davis 共同撰寫

程式碼代理程式及其使用方式在短短幾個月內就已經發生了巨大的演變。最初,重點主要放在觀察上——逐行審查每個輸出。但隨著模型能力迅速提升,業界轉向了真正的代理工程。如今,開發人員、PM 和設計師身兼多職,專注於崇高的概念目標,讓代理程式處理各個組件。

為了探索這個新領域,我們的團隊希望建立一個能展示這種確切工作流程的體驗。我們想建立一個遊戲,產生其資產,編寫行銷頁面,並部署整個項目,所有這些都使用 Google 首屈一指的 AI 原生平台:Antigravity。

Antigravity 將 Google 的最佳功能匯集於一處,採用規劃、執行和驗證的緊密回饋迴圈。它會建立 Artifact、撰寫程式碼、執行測試,甚至點擊 UI 中的按鈕,以確保它實際正確完成了任務。

我們代理程式冒險的成果是 DashLander——一款設定在程序生成的小行星上的月球著陸器風格遊戲。以下是我們如何建立它的故事。

在 AI 時代為何選擇 Flutter?

很多人可能會想:如果代理程式可以撰寫原生程式碼,為什麼不讓它們完全分開撰寫 Android、iOS 和 Web 應用程式呢?

這是一個合理的問題;這也是最初激發 Flutter 作為「Vibe once, run anywhere」UI 工具包概念的原因。

擁有單一真實來源對 AI 和人類都同樣重要。透過讓代理程式編寫單一的跨平台應用程式,團隊可以消除不同語言或平台特定範式之間不可避免地出現的細微錯誤。此外,Dart 強型別為 LLM 提供了出色的回饋。具有較鬆散型別系統的語言要求 LLM 進行更多的分析,才能知道給定程式碼在所有情況下是否正確。另一方面,Flutter 和 Dart 使用其分析伺服器向您的代理程式發送有關函數簽名或類別形狀不匹配的強烈訊號。搭配有狀態熱重載,Flutter 對代理程式的加速作用與對人類開發人員的加速作用一樣。

而且,儘管代理程式開發大幅降低了生產新軟體的成本,但它仍遠非免費。對於代理程式開發來說,時間就是金錢,這既是比喻意義上的,也是字面意義上的,因為查詢時間越長,意味著更多的令牌;這意味著更多的 AI 開支。有了 Flutter,成功完成編碼任務的代理程式無需重新開始支援另一個平台,從而為您節省了資金

快速失敗,才能建造更好

我們知道我們想建造一個遊戲,您在其中駕駛月球著陸器,配備動態元素、自訂著色器和粒子效果。(我們不忍心將 Dash 本身置於冰冷的真空空間,所以,您將駕駛一艘船。) 當然,使用 AI 大大加速了這些概念的整合,但這並不妨礙稍後放慢速度並認真編寫高品質程式碼。如果有的話,透過 AI 進行的快速探索階段有助於您更快地達到該階段,因為您的許多失敗實驗可以在幾個提示的生命週期內來去,而不是花費數小時或數天的編程時間,只是為了了解某個特定的想法是否真的很有趣。如果這輩子有一件事無關緊要,那就是從未見天日的實驗性功能的程式碼品質。

最重要的是,我們也知道我們沒有在建造什麼:一款高性能、即時的多人遊戲。即時多人遊戲功能會引入延遲限制並呈指數級擴大基礎設施複雜性。相反,我們意識到可以透過在挑戰模式中重播過去高分的回影來提供 90% 的引人入勝的競爭體驗。這使我們可以依賴靜態儲存和簡單的後端,並巧妙地免費重複使用完美編寫的「AI 對手邏輯」,只需重播最佳使用者已經做過的事情即可。這就是所有程式碼中最好的:您既不編寫也不維護的程式碼。

生成一個資產的宇宙

為了讓小行星著陸器遊戲變得有趣,我們需要出色的資產,我們在很大程度上依賴代理生態系統來獲取它們:

  • **音訊:** 我們使用 Google 的 Lyria 生成背景音樂。(不過,Rody 戴上他老音訊工程師的帽子,確實用麥克風、水管和噴霧罐在他家後院錄製了自訂的推進器噪音。多麼有趣的下午!)
  • **視覺效果:** 我們在 Stitch 和 Google Canvas 中生成了 UI 設計。Gemini 直接在程式碼中編寫了我們的粒子效果,我們使用 Nano Banana 生成了應用程式圖示。
  • **物理:** 因為這個遊戲發生在真空環境中,我們想要精確的反應控制系統 (RCS)。我們釋放了 Gemini Deep Research 來尋找我們需要的精確零大氣物理公式,它將這些公式編譯成 Google Doc。然後我們將該研究納入我們的語境中,Gemini 將方程式直接翻譯成 Dart。這些真的是正確的物理方程式嗎?可笑的是,我們不知道,因為我們不是物理學家!但是當您玩遊戲時,控制感肯定感覺正確。

**疊代原型**

我們從 AI Studio 的沙盒開始建立,以在提交架構之前生成快速、一次性的程式碼。這些豐富的原型可以作為模型以後的上下文壓縮,及早鎖定微觀決策。我們的一些原型具有更有趣的遊戲玩法,有些看起來更具吸引力,還有一些最清晰地捕捉了我們的零重力物理。對於我們的最終遊戲,我們將每個原型中最好的想法融合在一起。

一旦我們將專案載入 Antigravity,我們就為我們的代理程式配備了 Firebase、Flutter、Flame 遊戲引擎和 Gemini API 的 MCP 伺服器。我們啟動了一個新的提示,Antigravity 將任務分配給後端、UI 和遊戲邏輯的專門子代理程式。在五分鐘內,我們就有了一個用 Flutter 和 Flame 編寫的遊戲,這個遊戲通常需要幾天才能編寫。但實際上,它遠未完成。只有三個硬編碼的關卡,攝影機沒有跟隨著陸器,而且數學意味著我們的飛船大約有帝國大廈那麼大。

大約又用了 100 個提示才讓遊戲達到最終狀態。其中很大一部分提示只是關於程式碼組織和增加測試。每個人對閱讀 AI 生成的程式碼都有自己的看法,但 Rody 和我非常重視我所稱的「認知所有權」,並且仍然想了解我們應用程式的深層內部運作方式。我們閱讀了 Gemini 編寫的許多內容,並推動 LLM 進行重構以提高清晰度和可重用性。

我還必須面對一個嚴酷的現實:我不太擅長三角函數,而 DashLander 中確實有很多三角函數。當代理程式告訴我他們完美地實現了一個新的系統,例如著陸計算和得分時,我經常玩遊戲並立即發現差異。不幸的是,無論我在我的程式碼庫中閱讀 Gemini 密集的三角函數多少次,我都無法找出斷開點隱藏在哪裡。為了擺脫這個惡性循環,我要求 Gemini 在鍵盤快捷鍵後面建立一個絕密調試模式。它呈現了疊加層,顯示了確切的地形資料、表面的相對傾斜度和碰撞碰撞框,讓我能夠向自己證明代理程式的計算在哪裡是錯誤的。掌握了確鑿的證據,我能夠與 Gemini 合作解決了密集數學文件中的問題。

**時空旅行的棘手問題**

當我們加入挑戰模式時,Antigravity 在連接資料模型並將它們連接到 Cloud Firestore 方面做得非常出色。但在測試過程中,我發現了一個隱蔽的錯誤。我會與之前高分的鬼魂比賽,視覺上一切看起來都很棒,因為鬼魂飛船順利地漂移。但隨著重播的進行,事實證明鬼魂飛船與過去的自己不知不覺地脫節了。在比賽結束時,儘管原始玩家完美著陸,但鬼魂飛船有時會劇烈撞上小行星。

為了解決這個問題,我必須像 2023 年一樣,開動我的大腦。可怕,我知道。

我意識到核心問題是現代電腦架構的一個基本面向。您看,在現代多處理器 CPU 上,您可以告訴程式在未來精確的毫秒運行一段程式碼,但由於行程排程的性質,CPU 永遠無法保證完美精確度。而且,對我來說不幸的是,在重播期間啟動推進器的一毫秒延遲會從根本上改變飛船的模擬飛行路徑。我意識到,我不能僅僅記錄推進器時間戳,而是必須在每個推進器事件的精確時刻儲存著陸器物理狀態的完整表示,以校正重播期間的不準確性。Gemini 隨後整合了邏輯,以不斷將重播的即時模擬與這些物理檢查點進行比較,自動校正任何微小偏差。結果是完美無瑕、無崩潰的重播。

**從遊戲到發布**

您可能已經感受到在日常工作中身兼多職的壓力,無論您的團隊規模大小。例如,您可能擅長編寫遊戲,但不想推廣它們。或者,您是一位傑出的使用者體驗工程師,但更喜歡不處理圖形設計。好消息是,您不必獨自完成所有事情。

我們使用 Stitch 從單一提示生成了我們的行銷登陸頁面,並將我們正在運行的遊戲截圖作為上下文傳入。(誠然,如果生成優化的登陸頁面是我們的實際目標,我們會對此進行更多疊代。)使用一鍵匯出到 Antigravity,我們隨後下載了 HTML 並開始工作。因為我們是用 Dart 構建的,所以我們希望保持堆疊統一。我們使用 [Jaspr](https://pub.dev/packages/jaspr)(一個將 HTML 直接映射到 Dart 組件的 Web 框架)來無縫整合生成的 HTML。Gemini 完美處理了這種轉換,使我們能夠在遊戲和行銷網站之間共用程式碼,同時保留行業標準的 SEO 索引。

最後,我們使用 Antigravity CLI 來建構我們的 Flutter 網頁應用程式,將建構目錄複製到 Jaspr,執行建構腳本,然後直接部署到 Firebase Hosting——所有這些都在一個不間斷的終端工作流程中完成。

雖然 DashLander 目前以網路優先,以便輕鬆驗證機制,但由於它使用 Flutter 建構,我們只需一個指令即可部署到 iOS、Android、macOS、Windows、Linux,甚至豐田 RAV4 的資訊娛樂面板。截至撰寫本文時,該遊戲目前可在 [dashlander.com](http://dashlander.com) 上玩;但如果您在足夠遙遠的未來發現這篇文章,且該連結已失效,歡迎您直接從 [GitHub](https://github.com/craiglabenz/dashlander) 下載程式碼。

**實現您的願景**

若要開始建構您夢想中的應用程式或遊戲,請前往 [antigravity.google](https://antigravity.google) 獲取 Google 的 Agent Manager 和 IDE;並前往 [flutter.dev](https://flutter.dev) 開始使用 Flutter SDK。

祝您好運,我們迫不及待地想看看您會建立什麼!

0%