Dart 2.12 現已發布

作者 / Michael Thomsen
Dart 2.12 現已發布,其中包含 健全的空安全 和 Dart FFI 的穩定版。空安全是我們最新主打的一項生產力強化功能,意在幫助您規避空值錯誤,以前這種錯誤通常很難被發現,您可以觀看下面這支視頻了解詳情。FFI 則是一種互操作機制,支持調用以 C 語言編寫的既有代碼,例如調用 Windows Win32 API。歡迎大家即刻開始使用 Dart 2.12。
Dart 平臺的獨特功能
在詳細了解健全空安全和 FFI 之前,我們先來討論一下它們在哪些方面契合了我們對 Dart 平臺的期望。編程語言往往有很多類似的功能,例如,很多語言都支持面向對象的編程或在 web 上運行。真正將各個語言區分開來的,是其獨特的功能組合。

Dart 具有橫跨三個維度的獨特功能組合:
-
可移植性: 高效的編譯器可針對設備生成 x86 和 ARM 機器代碼,并針對 web 生成優化的 JavaScript。同時兼容移動設備、桌面 PC、應用后端等多種 目標平臺。大量的開發庫和 package 提供了可在所有平臺上使用的一致的 API,進一步降低了開發者創建真正多平臺應用的成本。
-
高生產力: Dart 平臺支持熱重載,因此可在原生設備和 web 上實現快速迭代開發。此外,Dart 還提供了豐富的結構,如 isolates 和 async/await 等,用以處理和實現常見的并發和事件驅動的應用模式。
-
穩健: Dart 的健全空安全類型系統可以在開發過程中就捕捉到錯誤。整個平臺擁有極好的可擴展性和可靠性,已經被大量且多樣的應用在累計超過十年的生產環境中實戰檢驗過,其中包括 Google 的一些關鍵業務應用,如 Google Ads 和 Google Assistant 等。
健全空安全增強了類型系統的穩健性,同時提高了性能。借助 Dart FFI,您可以獲得更強的可移植性,同時沿用由 C 語言編寫的既有代碼,在處理對性能要求極為嚴苛的任務時,可以盡情使用經過精心優化的 C 語言代碼。
健全的空安全
自 Dart 2.0 中引入健全類型系統以來,Dart 語言中最重大的新增內容便是健全空安全。空安全進一步增強了類型系統,讓您能夠捕捉到空值錯誤,此類錯誤經常導致應用崩潰。啟用空安全后,您就可以在開發過程中捕捉到空值錯誤,避免應用在生產環境中發生崩潰。
健全空安全的設計圍繞一套核心原則展開。您可以閱讀 官方文檔 了解這些原則對開發者的影響。
默認不可空: 從根本改變類型系統
在空安全出現之前,開發者面臨的核心挑戰在于無法區分預期收到空值的代碼和不接受空值的代碼。幾個月前,我們在 Flutter 的 master 渠道中發現了一個錯誤,多個 flutter 工具命令在特定計算機配置下會發生崩潰,并觸發空值錯誤: The method '>=' was called on null。問題出自如下代碼:
final int major = version?.major;
final int minor = version?.minor;
if (globals.platform.isMacOS) {
// plugin path of Android Studio changed after version 4.1.
if (major >= 4 && minor >= 1) {
...
您發現錯誤了嗎?由于 version 可能為空,所以 major 和 minor 也可能為空。如果單獨檢查此處代碼,這一錯誤似乎并不難發現。但實際上,即使經過了嚴格的代碼審查過程 (如 Flutter repo 所采用的代碼審查流程),也總是難免有這樣的漏網之魚。在啟用空安全后,靜態分析能夠立即捕捉到這一問題 (如下圖)。您可以 在 DartPad 中親自上手體驗。

△ IDE 中的分析結果
這只是一個非常簡單的錯誤。我們早期在 Google 內部的代碼中使用空安全時,捕捉到的復雜錯誤遠多于此。其中一些是多年前就已經發現的 bug,但在通過空安全進行額外的靜態檢查前,很多團隊都未能找到原因。
- 內部團隊發現,他們經常檢查表達式中是否存在空值,而這些表達式永遠不可能為空。這個問題在使用 protobuf 的代碼中最常見,其中可選字段在未經設置時會返回一個默認值,而且永不為空。這會導致代碼混淆默認值和空值,并錯誤地檢查默認條件。
- Google Pay 團隊在他們的 Flutter 代碼中發現了一些 bug,在嘗試訪問 Widget 上下文之外的 Flutter State 對象時會出錯。在采用空安全之前,這些對象會返回 null 并掩蓋錯誤;在采用空安全之后,健全分析確定這些屬性永遠不可能為空,并會給出分析錯誤。
- Flutter 團隊發現了一個 bug: 如果在 Window.render() 中向 scene 參數傳遞空值,則 Flutter 引擎可能會崩潰。在向空安全遷移的過程中,他們添加了一個提示,將 Scene 標記為不可空,即可輕松防止空值可能引發的應用崩潰。
在默認不可空的前提下工作
啟用空安全 后,聲明變量的基礎方法會發生變化,因為默認類型不可為空:
// 在空安全的 Dart 中,以下均不可為空
var i = 42; // Inferred to be an int.
String name = getFileName();
final b = Foo();
如果您想要創建可能同時包含值或 null 的變量,則需要在聲明變量時在類型后面顯式添加 ? 后綴:
// aNullableInt 可以為整型或 null
int? aNullableInt = null;
空安全的實現很穩健,并提供豐富的靜態流程分析,方便開發者輕松處理可空類型。例如,局部變量在進行空值檢查后,Dart 會將其類型從可空提升為非空:
int definitelyInt(int? aNullableInt) {
if (aNullableInt == null) {
return 0;
}
// aNullableInt 現在會被提示為非空 int
return aNullableInt;
}
我們還添加了一個新的關鍵字,required。當一個命名的參數被標記為 required (在 Flutter widget API 中經常出現),而調用者忘記提供該參數時,就會發生如下分析錯誤:

漸進遷移至空安全
空安全對于我們的類型系統而言是一項根本性的改變,因此如果我們執意強制所有開發者采用,勢必會造成嚴重的混亂。因此,我們想讓您自行決定合適的遷移時機,空安全將是一項可選特性: 在做好準備之前,您可以在無需強制啟用空安全的情況下使用 Dart 2.12。您甚至可以在尚未啟用空安全的應用或 package 中依賴已啟用空安全的 package。
為了幫助您將現有代碼遷移至空安全,我們提供了遷移工具和 遷移指南。該工具會首先分析您所有的代碼,然后您可以交互式地查看工具推斷出的可空屬性,如果您不同意工具得出的結論,則可以添加可空性提示以更改推斷。添加遷移提示可能會大幅提升遷移質量。

目前,在默認情況下,使用 dart create 和 flutter create 新創建的 package 和應用中不會啟用健全空安全。在大部分生態系統完成遷移后,我們預計將在后續的穩定版本中默認啟用。您可以通過 dart migrate 在新創建的 package 或應用中輕松 啟用空安全。
Dart 生態系統的空安全遷移狀態
去年,我們提供了健全空安全的數個預覽版和 Beta 版,旨在為生態系統提供首批支持空安全的 package。這項工作非常重要,我們建議大家 有序遷移至健全空安全,也就是說,在所有依賴項遷移完成之前,最好不要遷移自己的 package 或應用。
我們已發布由 Dart、Flutter、Firebase 和 Material 團隊所提供的數百個 package 的空安全版本。令人驚喜的是,Dart 和 Flutter 生態系統對此也予以巨大的支持,pub.dev 現在共有 1,000 多個 package 支持空安全。而且重要的是,最受歡迎的 package 已率先完成遷移,截止到 Dart 2.12 發布時,前 100 個最受歡迎的 package 中已有 98 個支持空安全,而在前 250 和前 500 的 package 中,支持空安全的比例則為 78% 和 57%。我們希望在接下來的幾周,pub.dev 上能夠出現更多支持空安全的 package。我們的分析 表明,pub.dev 上的絕大多數 package 已經可以 開始遷移。
充分健全的空安全的優勢
完成遷移后,您的項目就處于健全的空安全模式下了。這意味著 Dart 能夠完全確保具有不可空類型的表達式不為空。當 Dart 分析完您的代碼并確定某個變量不可為空時,該變量將始終不可為空。Dart 與 Swift 都擁有健全的空安全,但有些編程語言在這方面仍有待改進。
Dart 的健全空安全還暗含另一項備受期待的優勢: 您的程序可以更小、更快。由于 Dart 能夠確保不可為空的變量絕不為空,因此可以 實現優化。例如,Dart 的運行前 (ahead-of-time, AOT) 編譯器可以生成更小更快的原生代碼,因為當其知道變量不為空時,便不再需要添加空值檢查了。
Dart FFI: 集成 Dart 與 C 語言代碼庫
您可以通過 Dart FFI 調用 C 語言編寫的既有代碼庫,從而增強可移植性,還可以通過精心打磨的 C 代碼完成對性能要求極為嚴苛的任務。從 Dart 2.12 起,Dart FFI 已結束 Beta 測試階段,現已進入穩定狀態,可以用于生產環境。我們還添加了一些新功能,包括嵌套結構和按值傳遞結構。
按值傳遞結構
在 C 語言中,結構可通過引用和值進行傳遞。FFI 以前僅支持按引用傳遞結構,但從 Dart 2.12 開始,也支持按值傳遞。下方的簡單示例中,兩個 C 函數使用引用和值完成傳遞:
struct Link {
double value;
Link* next;
};
void MoveByReference(Link* link) {
link->value = link->value + 10.0;
}
Coord MoveByValue(Link link) {
link.value = link.value + 10.0;
return link;
}
嵌套結構
C API 通常使用嵌套結構,這種結構本身也包含結構,比如以下示例:
struct Wheel {
int spokes;
};
struct Bike {
struct Wheel front;
struct Wheel rear;
int buildYear;
};
從 Dart 2.12 起,FFI 將支持嵌套結構。
API 改動
作為 FFI 穩定版發布內容的一部分,并且為了支持上述功能,我們做了一些小幅的 API 改動。
現在不允許創建空結構 (重要改動參照 #44622),并會給出棄用警告。您可以使用一個新的類型 Opaque 來表示空結構。dart:ffi 函數 sizeOf、elementAt 和 ref 現在需要編譯時的類型參數 (重要改動參照 #44621)。因為在 package:ffi 中增加了新的便利函數,所以在常見的情況下,無需額外添加關于分配和釋放內存的模板代碼:
// 分配一個 Utf8 數組,使用 Dart 字符串填充,然后傳遞給 C 方法并轉換結果,最后釋放 arg
//
// API 變更前:
final pointer = allocate<Int8>(count: 10);
free(pointer);
final arg = Utf8.toUtf8('Michael');
var result = helloWorldInC(arg);
print(Utf8.fromUtf8(result);
free(arg);
// API 變更后:
final pointer = calloc<Int8>(10);
calloc.free(pointer);
final arg = 'Michael'.toNativeUtf8();
var result = helloWorldInC(arg);
print(result.toDartString);
calloc.free(arg);
自動生成 FFI 綁定
對于大型的 API 接口,編寫與 C 代碼集成的 Dart 綁定極其耗時。為減輕這一負擔,我們為大家準備了綁定生成器,可以通過 C 頭文件自動創建 FFI 封裝代碼,歡迎試用。
FFI 路線圖
核心 FFI 平臺完成后,我們的工作重心將轉向基于核心平臺擴展 FFI 功能集。我們正在研究的一些功能包括:
- ABI 特定數據類型,如 int、long、size_t (#36140)
- 結構中的內聯數組 (#35763)
- Packed 結構 (#38158)
- 聯合類型 (#38491)
- 對 Dart 開放終結方法 (finalizer) (#35770,請注意,您現在可以通過 C 語言使用終結方法)
FFI 使用示例
在過去的幾個月中,我們看到大家在使用 Dart FFI 集成一系列基于 C 語言的 API 時,發掘出了許多有創意的用法。下面介紹幾個示例:
- open_file 是一個用于在多個平臺打開文件的 API,使用 FFI 在 Windows、macOS 和 Linux 上調用操作系統原生 API。https://pub.flutter-io.cn/packages/open_file
- win32 封裝了最常用的 Win32 API,便于從 Dart 直接調用各種 Windows API。
- objectbox 是一個快速數據庫,底層由 C 語言實現。
- tflite_flutter 使用 FFI 封裝了 TensorFlow Lite API。
Dart 語言的未來計劃
健全空安全是這幾年我們對 Dart 語言做出的最大改變。接下來,我們將繼續穩步改進 Dart 語言和平臺。下面簡單介紹一些我們在 語言設計規劃 中實驗的內容:
- 類型別名 (#65): 將創建類型別名的功能擴展到非函數類型。例如,您可以創建一個 typedef 并將其用作變量類型:
typedef IntList = List<int>;
IntList il = [1,2,3];
-
無符號右移運算符 (#120): 添加新的 >>> 運算符,便于對整數執行無符號移位操作。此運算符可完全重寫。
-
通用元數據注解 (#1297): 擴展元數據注解,以同時支持含類型參數的注解。
-
靜態元編程 (#1482): 支持靜態元編程,即編譯期間生成新 Dart 源代碼的 Dart 程序,與 Rust 宏 (macro) 和 Swift 函數構建器 (function builder) 類似。該功能目前仍處于早期探索階段,但我們認為它可能會開啟全新用例的大門,打破現在依賴代碼生成的僵局。
Dart 2.12 現已發布
歡迎大家下載 Dart 2.12 和 Flutter 2.0 SDK,即刻開始使用 Dart 2.12,盡情體驗健全空安全和穩定版 FFI。請大家閱覽 Dart 和 Flutter 的已知空安全問題。如果您發現其他任何問題,請在 Dart 問題跟蹤頁 中報告給我們。
如果您已在 pub.dev 上發布了 package,請立即參閱 遷移指南,了解如何遷移至健全空安全。遷移有助于依賴您的 package 的其他 package 和應用完成遷移。我們在此向已經完成遷移的開發者們表示感謝!
歡迎大家與我們分享自己的健全空安全和 FFI 體驗,我們評論區見!
浙公網安備 33010602011771號