動態

詳情 返回 返回

Dart宏被砍掉的真相:為什麼Go、Python、Java等高級語言都拒絕宏? - 動態 詳情

哈嘍,我是老劉

前兩天的文章講了老劉對Dart宏功能的期待和Dart官方取消宏的一點觀點。

Dart的宏取消了,期待3年的功能,説沒就沒了?

有人評論説高級編程語言是不需要宏功能的。
在這裏插入圖片描述

雖然老劉自己是非常支持宏的,但是不得不説這個觀點其實也是有一定的道理的。

為啥這麼説呢?

接下來我就來對比一下C語言的宏和Dart的build_runner,看看各自的優劣在哪裏。

相信對比完大家也就能理解兩種處理代碼的方式各自的優劣,以及Dart團隊在宏功能上的野望。

相同點:編譯時代碼生成的共同優勢

雖然C語言宏和Dart的build_runner在實現機制上截然不同,但它們的目標卻是一致的。

都是為了在編譯之前就把事情搞定,避免運行時的各種開銷。

第一個共同點:編譯前代碼生成

想象一下,你寫了一個JSON序列化的代碼。

如果用運行時反射,每次序列化都要通過反射去讀寫對象的屬性。

但如果用編譯前代碼生成,編譯器直接幫你生成好了專門的序列化代碼。

運行時直接調用,速度肯定是提升了很多。

第二個共同點:告別樣板代碼地獄

寫過Java的同學都知道,getter和setter能把人寫到懷疑人生。

寫過C++的同學也知道,各種重複的函數聲明和實現能讓人抓狂。

C語言宏可以這樣解決:

#define GETTER_SETTER(type, name) \
    type get_##name() { return this->name; } \
    void set_##name(type value) { this->name = value; }

Dart的build_runner也能實現類似的功能,比如json_annotation:

@JsonSerializable()
class User {
  final String name;
  final int age;
}

編譯器自動生成fromJson和toJson方法。

一行註解,省下幾十行代碼。

關鍵差異:實現機制的不同

説完了相同點,咱們來聊聊最核心的差異。

文本替換 vs 語義理解:兩種截然不同的技術路線

C語言宏:文本替換

C語言的宏本質上就是一個文本替換工具。

編譯器在詞法分析之前,預處理器就把宏展開了。

舉個例子:

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

double area = PI * SQUARE(radius);

預處理器會直接替換成:

double area = 3.14159 * ((radius) * (radius));

C語言的宏本質上就是文本替換。

預處理器不理解代碼語義,只是把宏名替換成宏體。

當然這個替換不是簡單機械的,而是可以通過條件判斷來實現不同場景的替換。

Dart build_runner:基於AST的智能生成

Dart的build_runner完全不同。

它工作在AST(抽象語法樹)層面,真正理解Dart語言的語法和語義。

比如json_serializable這個包:

@JsonSerializable()
class User {
  final String name;
  final int? email;
  final DateTime createdAt;
}

build_runner會:

  1. 解析這個類的AST結構
  2. 理解每個字段的類型
  3. 知道哪些字段可以為null
  4. 生成類型安全的序列化代碼

生成的代碼可能是這樣的:

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      'name': instance.name,
      'email': instance.email,
      'createdAt': instance.createdAt.toIso8601String(),
    };

注意,它知道DateTime需要調用toIso8601String()方法。

這就是語義理解的威力。

類型安全性:天壤之別

C語言宏最大的問題就是沒有類型檢查。

#define SWAP(a, b) { typeof(a) temp = a; a = b; b = temp; }

int x = 5;
char* y = "hello";
SWAP(x, y);  // 預編譯通過,但邏輯錯誤

而Dart的build_runner生成的代碼是完全類型安全的:

// 如果你的字段類型不支持JSON序列化
class User {
  final File file;  // File類型無法序列化
}

build_runner會直接報錯,告訴你這個類型不支持序列化。

錯誤信息清晰明瞭,開發時就能發現問題。

作用域和衞生性

C語言宏還有一個致命問題:不衞生(non-hygienic)。

#define SWAP(a, b) { int temp = a; a = b; b = temp; }

int main() {
    int temp = 10;
    int x = 5, y = 7;
    SWAP(x, y);  // temp變量衝突!
}

宏展開後:

int main() {
    int temp = 10;
    int x = 5, y = 7;
    { int temp = x; x = y; y = temp; }  // 變量名衝突
}

這種問題在複雜項目中就可能會造成很多難以定位的bug。

Dart的build_runner完全不會有這個問題。

它生成的代碼遵循正常的Dart作用域規則,不會產生任何意外的變量捕獲。

生成的代碼就像手寫的一樣規範。

C語言宏的獨特優勢

説了這麼多build_runner的好處,是不是覺得C語言宏已經過時了?

別急,老劉要為C語言宏正名了。

有些事情,build_runner真的做不到,而C語言宏卻能輕鬆搞定。

真正的條件編譯

最典型的例子就是DEBUG_PRINT。

在C語言中,你可以這樣寫:

#ifdef DEBUG
#define DEBUG_PRINT(x) printf("Debug: %s\n", x); expensive_memory_check()
#else
#define DEBUG_PRINT(x) // 完全消失,不產生任何代碼
#endif

注意這裏的關鍵:在Release模式下,DEBUG_PRINT(x)這行代碼完全不存在。

不是被優化掉,不是被跳過,而是根本就沒有這行代碼。

連函數調用的開銷都沒有,連參數計算的開銷都沒有。

這就是真正的條件編譯。

Dart的替代方案

Dart當然也有類似的機制,比如kDebugMode常量:

if (kDebugMode) {
  print('Debug: $message');
  expensiveMemoryCheck();
}

看起來很像對吧?

但是有個問題:雖然kDebugMode是編譯時常量,但是這些代碼是真實存在的。

雖然可以通過Dart的tree-shaking能力來移除這些代碼,但是這個效果還是有限的。

特別是當你的調試代碼比較複雜時,編譯器不一定能完全優化掉。

而且另一個問題是每次打印debug信息都需要手動添加kDebugMode判斷,真正寫過代碼的人都知道這是個非常麻煩的事。

還有assert語句:

assert(() {
  print('Debug: $message');
  expensiveMemoryCheck();
  return true;
}());

assert在release模式下確實會被完全移除。

但是語法太不優雅了,而且使用場景受限。

你總不能把所有調試代碼都塞到assert裏面吧?

build_runner的根本侷限

為什麼build_runner做不到真正的條件編譯?

這是由它的工作機制決定的。

時機問題:build_runner是在編譯前生成代碼,它無法根據編譯模式(debug/release)來差異化生成代碼。

作用域限制:build_runner只能生成新的文件,無法在調用點進行條件替換。

語義層面:build_runner工作在AST(抽象語法樹)層面,而條件編譯需要更底層的預處理器支持。

舉個例子,你想用build_runner實現DEBUG_PRINT,最多隻能生成這樣的代碼:

void debugPrint(String message) {
  if (kDebugMode) {
    print('Debug: $message');
  }
}

但這樣的話,函數調用本身還是存在的。

參數的計算也還是存在的。

如果你傳給debugPrint的調試信息需要複雜計算,這些計算在release模式下依然會執行。

性能敏感場景的差異

在一些性能敏感的場景下,這種差異就很明顯了。

比如遊戲引擎的渲染循環,每幀可能要執行數萬次的調試檢查。

C語言宏可以讓這些檢查在release版本中完全消失。

而Dart的方案,即使被優化,也可能留下一些痕跡。

當然,對於大多數應用開發來説,這點性能差異可能不重要。

但在某些特定場景下,這就是C語言宏不可替代的優勢。

開發者體驗

説完了技術層面的差異,我們再來聊聊開發者體驗。

C語言宏:無感知的完美體驗

C語言宏最牛逼的地方,就是你完全感覺不到它的存在。

寫代碼的時候,你就像寫普通代碼一樣。

編譯的時候,編譯器自動幫你處理好一切。

IDE的語法高亮、代碼補全、錯誤提示,全都是即時可用的。

你不需要記住任何額外的命令。

不需要等待任何生成過程。

不需要擔心生成的代碼是否最新。

舉個例子,你在寫一個C項目,定義了一個DEBUG_PRINT宏:

#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) printf("Debug: " fmt "\n", ##__VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif

然後在代碼裏直接用:

DEBUG_PRINT("User login: %s", username);

IDE立馬就能識別這個宏,給你語法高亮。

編譯的時候,根據是否定義了DEBUG,這行代碼要麼變成printf調用,要麼完全消失。

整個過程,你作為開發者完全不需要干預任何事情。

build_runner:需要人工干預

再看看Dart的build_runner,體驗就完全不同了。

首先,你得記住一堆命令:

dart run build_runner build    # 生成代碼
dart run build_runner watch    # 監聽文件變化
dart run build_runner clean    # 清理生成的文件

這還只是基礎的。

如果你想要增量構建,你得用watch模式。

但watch模式有時候會卡住,你得手動重啓。

更要命的是,在代碼生成之前,IDE會顯示一堆錯誤。

比如你寫了這樣的代碼:

@JsonSerializable()
class User {
  final String name;
  final int age;
  
  User({required this.name, required this.age});
  
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

在你運行build_runner之前,IDE會告訴你:

"_$UserFromJson未定義"
"_$UserToJson未定義"

滿屏的紅色波浪線。

新手看到這種情況,第一反應可能是:我是不是寫錯了什麼?

IDE集成的不完善

雖然現在的IDE(比如VS Code、Android Studio)對build_runner有一定的支持。

但遠遠達不到C語言宏那種無縫集成的程度。

比如,你在VS Code裏修改了一個帶註解的類。

IDE不會自動觸發代碼生成。

你得手動運行命令,或者依賴watch模式。

而watch模式有時候會出現問題:

  • 文件變化沒有被檢測到
  • 生成過程卡住了
  • 和其他工具衝突

這些問題,在C語言的宏系統裏是不存在的。

總結一下,在開發者體驗方面:

C語言宏

  • 零干預,完全透明
  • IDE支持完美
  • 無需記憶額外命令
  • 無穩定性問題

build_runner

  • 需要手動執行命令
  • IDE集成不完善
  • 需要學習和記憶工作流程
  • 存在穩定性和性能問題
  • 多包項目複雜性高

這就是為什麼很多開發者懷念宏功能的原因。

不僅僅是因為build_runner功能不夠強大。

還是因為它在用户體驗上,確實有很大的改進空間。

相信看到這裏大家也能明白當初dart團隊對宏的野心有多大了,他們既想要build_runner的先進性,也想要C語言宏的各種優勢。

但是通過前面的對比我們可以發現,這兩者各自的優勢都直指對方的劣勢。

而這種結果都是源於他們底層的實現邏輯。

想要不付出任何代價的集合兩者的優勢,同時又不引入任何的副作用,似乎有點異想天開。

我想可能這也就是為什麼Dart的宏功能在這樣龐大的構想下,開發中碰到了各種難以解決的問題,並最終被放棄的底層原因。

七、結論:技術選擇的多維度思考

通過前面的深度對比,老劉想説一個很重要的觀點:

技術沒有絕對的先進和落後,只有適合和不適合。

很多人一聽到"宏"這個詞,就覺得這是古老的、落後的技術。

但事實上,在某些特定場景下,C語言宏的簡潔和高效,是build_runner無法比擬的。

同樣,很多人覺得build_runner複雜、麻煩。

但在類型安全、語義理解、代碼質量方面,它確實比傳統宏要先進得多。

也許未來的技術發展方向,就是在保持先進性的同時,不斷優化開發者體驗。

讓複雜的技術變得簡單易用,讓強大的功能變得透明無感。

這可能才是技術進步的真正意義。

真正的技術高手,不是隻推崇一種方案的專家,而是能在不同場景選擇最適合工具的智者。技術的先進性不僅體現在功能上,更體現在對開發者體驗的關注上。

好了,如果看到這裏的同學對客户端、Flutter開發或者MCP感興趣,歡迎聯繫老劉,我們互相學習。
點擊免費領老劉整理的《Flutter開發手冊》,覆蓋90%應用開發場景。
可以作為Flutter學習的知識地圖。

覆蓋90%開發場景的《Flutter開發手冊》

user avatar huang-yi-san 頭像
點贊 1 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.