0%

【文章內容使用 Gemini 1.5 Pro 自動產生】

Flutter 應用程式的效能測試

Flutter 框架在預設情況下非常快速,但這是否意味著您不必考慮效能呢?不。絕對有可能撰寫出速度緩慢的 Flutter 應用程式。另一方面,也有可能充分利用框架,讓您的應用程式不僅速度快,而且高效,消耗更少的 CPU 時間和電池。

這就是我們想要的:一個在某些有意義的指標上比較您的應用程式的兩個版本時具有統計顯著性的結果。請繼續閱讀以了解如何獲得此結果。

Flutter 效能優化的概略準則如下:

  • 更新狀態時,目標盡可能少的 Widget。
  • 僅在必須時更新狀態。
  • 將計算密集型任務放在建構方法之外,理想情況下是在主隔離區之外。

事實上,對於許多有關效能优化的問題,答案是「視情況而定」。對 特定 Widget 進行的 特定 優化是否值得付出努力和維護成本?特定 情況下的 特定 方法是否有意義?

這些問題唯一有用的答案來自測試和測量。量化每個選擇對效能的影響,並根據這些資料做出決定。

好消息是,Flutter 提供了出色的效能分析工具,例如 Dart DevTools(目前處於預覽發行階段),其中包含 Flutter Inspector,或者您可以直接從 Android Studio(安裝 Flutter 外掛後)使用 Flutter Inspector。您有 Flutter Driver 用於測試您的應用程式,以及用於儲存效能資訊的 Profile 模式。

壞消息是現代智慧型手機非常「聰明」。

治理器的問題

iOS 和 Android 治理器使量化 Flutter 應用程式的效能變得特別困難。這些系統級別的守護程序會根據負載調整 CPU 和 GPU 單位的速度。當然,這大多是好事,因為它確保在消耗盡可能少的電池的情況下提供流暢的效能。

缺點是,您可以透過讓應用程式 更多 地工作來使其顯著 更快

在下方,您可以看到如何將具有無意義 print 語句的迴圈加入到應用程式中,使治理器切換到更高檔,從而使應用程式更快,其效能也更可預測。

治理器的問題:預設情況是,您無法信任您的數字。在這個箱型圖中,我們在 x 軸上有單獨的執行(以它們開始的精確時間標記),以及 y 軸上的建構時間。如您所見,當我們加入一些完全不必要的 `print` 語句時,它會使建構時間下降,而不是上升。

在此實驗中,較差的程式碼導致更快的建構時間(上方)、更快的柵格化時間和更高的畫面更新率。當客觀上較差的程式碼導致更好的效能指標時,您便無法依賴這些指標來提供指導。

這只是一個例子,說明行動應用程式的效能基準測試為何會不合常理且困難。

在下方,我分享了一些我在處理 Flutter 的 Google I/O 應用程式 Developer Quest 時收集的技巧。

基本建議

  • 在 DEBUG 模式下不要測量效能。僅在 profile 模式下測量效能。
  • 在真實設備上測量,而不是在 iOS 模擬器或 Android 模擬器中。軟體模擬器非常適合開發,但它們與真實設備的效能特性有很大差異。Flutter 不允許您在模擬設備上以 profile 模式執行,因為這沒有任何意義。您以這種方式收集的資料不適用於現實世界的效能。
  • 理想情況下,使用相同的物理設備。將其作為您的專用效能測試設備,不要用於其他任何用途。
  • 了解 Flutter 的 效能分析工具

CPU/GPU 治理器

如上所述,現代作業系統會根據負載和其他一些啟發式算法更改其處置的每個 CPU 和 GPU 的頻率。(例如,觸碰螢幕通常會使 Android 手機切換到更高檔。)

在 Android 上,您可以關閉這些治理器。我們稱此過程為「比例鎖定」。

  • 建立一個比例鎖定效能測試設備的腳本。您可以使用 Skia 的做法 作為靈感。您也可以查看 Unix CPU API
  • 除非您執行的基準測試作業規模龐大,例如 Skia,否則您可能想要更輕量級且不那麼通用的工具。查看 Developer Quest 的 shell 腳本 以獲取一些提示。例如,以下摘錄將 CPU 設為使用者空間治理器(唯一不會自行更改 CPU 頻率的治理器)。
1
2
3
4
5
6
7
#!/usr/bin/env bash

GOV="userspace"
echo "Setting CPU governor to: ${GOV}"
adb shell "echo ${GOV} > /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"
ACTUAL_GOV=`adb shell "cat /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"`
echo "- result: ${ACTUAL_GOV}"
  • 您的目標在此不是模擬現實世界的效能(沒有使用者會比例鎖定其設備),而是讓不同執行之間的效能指標具有可比性。
  • 最終,您需要進行實驗,並將 shell 腳本調整為您將使用的設備。這項工作很繁瑣,但直到您完成這項工作之前,您的效能資料都會欺騙您。
Developer Quest 的早期版本在桌面上由 Flutter Driver 測試。

Flutter Driver

Flutter Driver 讓您可以自動測試您的應用程式。請閱讀 flutter.dev 的 效能分析 部分,以獲取有關如何在分析應用程式時使用它的具體做法。

  • 在效能測試時,不要手動測試您的應用 Programm。始終使用 Flutter Driver 以確保您比較的是同樣的事物。
  • 撰寫 Flutter Driver 程式碼,讓它測試您真正想要測量的內容。如果您追求的是一般的應用程式效能,請嘗試瀏覽應用程式的各個部分,並執行使用者會執行的操作。
  • 如果您的應用程式包含機率因素(隨機、網路事件等),請將其模擬出來。這些瀏覽過程應盡可能彼此相似。
  • 如果需要,請使用 TimelinestartSync()finishSync() 方法來新增自訂時間軸事件。例如,當您感興趣的是特定函數的效能時,此方法非常有用。將 startSync() 放在其開頭,將 finishSync() 放在其結束位置。
  • 儲存摘要(writeSummaryToFile)和更重要的是原始時間軸(writeTimelineToFile)。
  • 對於應用程式的每個版本,請執行多次測試。對於 Developer Quest,我將其收斂到 100 次執行。(當您測量雜訊較大的事物時,例如第 99 個百分位數,您可能需要更多次執行。)對於基於 POSIX 的系統,這只意味著執行類似以下內容:for i in {1..100}; do flutter drive --target=test_driver/perf.dart --profile; done
使用 Chrome 的時間軸工具檢查 Flutter 的 profile 輸出。

時間軸

時間軸是 profile 執行結果的原始輸出。Flutter 將此資訊轉儲到一個 JSON 檔案中,可以將其載入到 chrome://tracing 中。

  • 了解如何在 Chrome 的追蹤時間軸中打開完整時間軸。您只需要在 Chrome 瀏覽器中打開 chrome://tracing,點選「載入」,然後選擇 JSON 檔案。您可以在 這個簡短的教學課程 中了解更多資訊。(Flutter 也提供了 時間軸工具,目前處於技術預覽階段。我沒有使用它,因為 Developer Quest 專案在 Flutter 的時間軸工具準備就緒之前就已經開始了。)
  • 使用 WSAD 鍵在 chrome://tracing 中移動時間軸,並使用 1234 切換操作模式。
  • 首次設定效能測試時,請考慮使用完整的 Android 系統追蹤來執行 Flutter Driver。這為您提供了更多關於設備中實際發生的事情的洞察力,包括 CPU 比例資訊。但是,請不要在完全開啟系統追蹤的情況下測量您的應用程式,因為它會使一切都變得更慢且更不可預測。
  • 如何使用 Flutter Driver 執行完整的 Android 系統追蹤?首先,使用 /path/to/your/android/sdk/platform-tools/systrace/systrace.py --atrace-categories=gfx,input,view,webview,wm,am,sm,audio,video,camera,hal,app,res,dalvik,rs,bionic,power,pm,ss,database,network,adb,pdx,sched,irq,freq,idle,disk,load,workq,memreclaim,regulators,binder_driver,binder_lock 開始 Android 系統追蹤。然後,使用 flutter run test_driver/perf.dart --profile --trace-systrace 啟動應用程式。最後,使用 flutter drive --driver=test_driver/perf_test.dart --use-existing-app=http://127.0.0.1:NNNNN/(其中 NNNNN 是 flutter run 上面的埠號)啟動 Flutter Driver。

指標

查看盡可能多的指標總比少好,但我發現有些指標比其他指標更有用。

  • 建構時間和柵格化時間(TimelineSummary 預設提供的指標)僅適用於實際上不包含太多 UI 建構以外的內容的非常嚴格的效能測試。
  • 不要將 TimelineSummary.frameCount 作為計算畫面更新率 (FPS) 的方法。Flutter 的 profile 工具不會提供實際的畫面更新率資訊。TimelineSummary 提供 countFrames() 方法,但它只計算已完成的畫面建構次數。一個經過良好優化的應用程式會限制不必要的重新建構,其畫面更新率將低於頻繁重新建構的未優化應用程式。
  • 我個人透過測量執行 Dart 程式碼所花費的總 CPU 時間來獲得最有用的資料。這計算了在建構方法中以及在建構方法之外執行的程式碼。假設您在比例鎖定的設備上執行 profile 測試,則總 CPU 時間是應用程式將消耗多少電池電量的良好近似值。
  • 找出執行 Dart 程式碼所花費的總 CPU 時間最簡單的方法是測量時間軸中 MessageLoop:FlushTasks 事件的範圍。對於 Developer Quest,我撰寫了一個 Dart 工具 來提取這些事件
  • 若要偵測卡頓(即跳過的畫面),請尋找極端值。例如,對於 Developer Quest 的特定案例和我們用於測試的設備,查看第 95 個百分位數的建構時間很有幫助。(即使比較具有截然不同的效率水準的程式碼,第 90 個百分位數的建構時間也過於相似,而第 99 個百分位數的數字往往會出現雜訊。您的情況可能有所不同。)
  • 如上所述,請對應用程式的每個版本執行多次測試(可能 100 次)。然後,使用具有誤差範圍的平均值或百分位數資料。更好的是,使用箱型圖。

結果

設定完畢後,您便能夠自信地比較提交和實驗。在下方,您可以看到針對常見困境的答案:「這個優化是否值得維護開銷?」

我認為在 特定 案例中,答案是肯定的。只需增加幾行程式碼,我們應用程式的每次自動瀏覽平均可以節省 12% 的 CPU 時間。

但是,這是本文的主要訊息 - 另一項優化的測量結果可能會顯示出截然不同的情況。試圖過度外推效能測量結果是引人入勝的,但錯誤的做法。

換句話說:「視情況而定」。我們應該接受這句話。


Flutter 應用程式的效能測試 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

http://creativecommons.org/licenses/by/4.0/

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

宣佈 Dart 2.3:針對建構使用者介面最佳化

今天,我們宣佈 Dart 2.3 SDK 的穩定版本發佈,其中包含新的語言功能,可改善您在開發使用者介面時的編碼體驗,新的工具支援用於開發 Flutter UI,以及兩個新的網站:dart.devpub.dev

Dart 的發展勢頭

我們每年都期待的開發者研究報告之一是 StackOverflow 開發者調查,它提供了關於開發者趨勢和對不同技術的看法的一個綜合數據集。今年的版本展示了 Dart 的人氣和知名度的增長,它首次進入了 最受歡迎的語言 名單,與其他流行語言如 JavaScript、C# 和 Go 並列,並領先於 C++、F# 和 R 等語言。同時,我們在 Flutter 社群的好朋友在 最受歡迎的框架 名單中排名第三。上個月 Codementor 關於 學習和不學習哪些程式語言 的調查也傳達了類似的正面消息:

兩個真正名列前茅的、進步最大的語言的例子是 Dart 和 Ruby」。 Codementor,2019 年 4 月 來源

我們要向 Dart 社群的所有開發者致以誠摯的 感謝。看到您採用 Dart、提供您的回饋,並在我們嘗試建構用於在任何平台上運行快速應用程式的最佳客戶端優化語言的過程中繼續與我們同行,這對我們來說意義重大。

用於建構使用者介面的新語言功能

談到客戶端開發,Dart 和 Flutter 團隊之間一項長期合作的項目是為使用 Dart 捕捉使用者介面程式碼創造良好的支援,而無需標記語言。我們認為,對行為和佈局使用單一語言具有很大的優勢。這些優勢包括減少上下文切換、無需學習兩種語言,以及在建構 UI 時允許使用通用程式語言的所有抽象功能。

在過去的幾個版本中,我們進行了一些改進,例如簡化 建構 widget 的程式碼、加入自動 int 到 double 的轉換,以及加入 集合字面量。在 Dart 2.3 中,我們又向前邁出了一大步,提供了三個新功能,用於表達基於列表、條件或重複的 UI。

您可以將 UI 視為 widget 節點的樹狀結構。某些節點包含 widget 列表,例如可捲軸元素的列表。通常,這些列表是由其他列表建構的。為此,我們加入了一個新的 展開運算符 功能,用於將一個列表中的元素 解包 到另一個列表中。在下面的範例中,buildMainElements() 返回一個 widget 列表,然後使用展開運算符 ... 將其解包到周圍的列表中:

另一個常見的 UI 任務是根據條件包含特定元素。例如,您可能希望在除最後一頁之外的所有頁面上都包含一個 下一步 按鈕。使用 Dart 2.3,您可以使用 集合 if 來做到這一點:

最後,UI 通常會從其他重複的元素建構重複的元素。您可以使用新的 集合 for 功能來表達這一點:

而且由於這三個新功能是語言功能,而不是標記命令,因此它們是完全可組合的,並且可以在您處理集合的任何上下文中使用。這些功能包含在 Flutter 1.5 版本中,並且在您可以今天下載的 Dart 2.3 版本中提供。我們還加入了新的 lint,您可以在 靜態分析中設定 以強制使用新的 spread集合 if集合 for 功能。

有關加入這些功能的所有工作的詳細資訊,請查看 Dart 語言團隊工程師 Bob Nystrom(又名 munificentbob)的 最近的這篇文章

我們還要感謝參與 UX 研究的開發者,這些研究對於塑造這些新的語言功能至關重要。

IDE 和編輯器功能

為了與 Dart 2.3 的 UI 主題保持一致,我們還在 IDE 支援中加入了新的 UI 指南功能。UI 指南是在 UI 程式碼中繪製的水平和垂直線,可以更輕鬆地查看 Flutter UI build() 方法的樹狀結構。以下是一個範例(來自計算機應用程式),其中 UI 指南清楚地說明了 UI 是由一個展開的 Column 建構的,其中包含多個 KeyRow,每個 KeyRow 都包含 NumberKey。

IDE with UI Guides visualizing the tree structure of UI code

UI 指南在 IntelliJ IDEA 和 Android Studio plugin 的 版本 35.2 中提供。要啟用此功能,請選擇設定 偏好設定 > 語言和框架 > Flutter > UI 指南。我們希望在後續版本中在 VS Code 中提供類似的支援。

最後,我們觀察到開發者經常使用 IDE 中的程式碼完成作為探索 API 的一種方式。程式碼完成對於探索您已匯入的函式庫中的 API 非常有效,但它不適用於尚未匯入的函式庫中的 API。我們的工具現在可以支援後一種使用案例:您可以在任何前綴上叫用程式碼完成,並且您將看到目前套件、它直接依賴的套件和 SDK 中所有 API 的完成。如果您從尚未匯入的函式庫中選擇一個完成(標記為 自動匯入,如下面的動畫所示),則工具會為您加入匯入語句。

Animation showing code completion & automatic import adding

這個新的自動匯入功能在 VS Code 中的 v2.26 plugin、IntelliJ 2019.1 和即將發佈的 Android Studio 3.5 版本中提供。

新的 Dart 和 Pub 網站

最後,但同樣重要的是,在過去的幾個月中,我們一直忙於為 Dart 平台建構一個新的網站:dart.dev

Redesigned https://dart.dev homepage

這個新的網站提供了一個全新的首頁,重點是解釋 Dart 平台的核心優勢。我們還更新了文件頁面,使其具有更好的導航和更具視覺吸引力。最後,我們對所有內容進行了大量的重新組織,以便更容易發現,並為以前缺少的核心內容加入了新的頁面。

同樣地,我們也對 Pub 套件網站進行了視覺更新,並將其移至一個方便的新網址:pub.dev

我們很樂意聽到您對這兩個網站的回饋。如果您發現問題或有任何建議,請在 dart.dev 問題追蹤器pub.dev 問題追蹤器 中建立一個問題。感謝您的支援!


宣佈 Dart 2.3:針對建構使用者介面最佳化 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動產生】

Flutter:為行動、網頁和桌面打造美麗的應用程式

今年在 I/O ‘19 中,我們發佈了有關 Flutter 的無數消息,如果您沒有全職追蹤所有消息,您可能會錯過其中一些。因此,這篇文章將彙集所有重大消息,以及來自新聞報導的重點。

在 Google I/O '19 開發者大會主題演講中,Adam Seligman 宣布 Flutter for Web 技術預覽版。

TL;DR

Flutter 從行動設備擴展到支援網頁和桌面:相同的程式碼庫,同樣關注於高生產力下快速、美麗的體驗。

新聞和公告

來自 Flutter 和 Dart 團隊的文章

來自合作團隊的文章

Flutter 和 Dart 主題演講和會議錄影

示範和 Codelabs

紐約時報 KENKEN 遊戲,顯示在 Chrome、macOS、行動 Safari、iPhone 和 Android 上運行。

新聞報導重點

  • “Flutter 已經取得了長足的進步,迅速成為跨 iOS 和 Android 的多平台編碼最佳框架之一。[…] 坦白說,對 Flutter 的積極反應讓人感到震驚。” (XDA 開發者)
  • “Google 的 Flutter UI 工具包專注於跨平台開發,可能只有兩年的歷史,但它已迅速成為許多開發人員的首選框架。” (TechCrunch)
  • “Google 正在解決應用程式開發人員面臨的最大障礙之一,這不僅讓開發人員的工作變得更輕鬆,而且讓他們的應用程式和服務在我們最常用的幾乎所有平台上都更易於使用,包括 Android、iOS、Windows 10、macOS、Chrome OS 和 Web 瀏覽器。” (BusinessInsider)
  • “顯然,Google 正在努力讓 Flutter 成為建立各種應用程式的最佳方式。無論您的應用程式是針對 Android、iOS、Chrome OS、Windows、Web、IoT 還是所有這些,都不再重要了,這真是太驚人了。” (9to5Google)
  • “這些更新今天宣布時收穫了許多歡呼聲,……意味著 Flutter 實際上成為了多平台 UI 框架。” (SiliconAngle)

請參閱 VentureBeatZDNetThurrott 等其他媒體報導。


Google I/O 上 Flutter 新聞彙總 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

宣布 Travis CI 上 Dart 的 Windows 支援

最近,Travis CI——一個用於 GitHub repos 的持續整合 (CI) 服務——宣布 對 Windows 作業系統的早期版本支援。今天,我們宣布 Windows 上的 Travis CI 支援 Dart SDK,因此您可以在所有三大桌面作業系統:Linux、Mac 和 Windows 上運行 Dart 建置和測試作業。

為多個作業系統設定 Travis CI 作業

如果您已經使用 Travis CI 建置或測試您的 Dart 程式碼,您可以輕鬆地在所有三個作業系統上運行您的作業。只需在您的 repo 的 .travis.yml 檔案中加入(或擴展)os: 部分:

1
2
3
4
os:
- linux
- osx
- windows

然後,每當 Travis CI 運行時,您都會看到在每個啟用的作業系統上運行的每個測試設定的項目。以下是來自 grpc-dart repo 的範例,在我們將 Windows 加入測試矩陣之後:

在 Linux、MacOS 和 Windows 上運行的 Travis CI 測試矩陣

由於 Travis CI 上的 Windows 支援仍處於早期版本階段,我們建議您在將 Windows 加入到 Travis 作業之前,先查看已知問題

將 Travis CI 測試支援加入到專案

如果您尚未使用 Travis CI 測試 Dart,則很容易開始使用。最小的 .travis.yml 檔案只包含一行:

1
language: dart

此設定等同於 pub run test

dart_task: 標籤下列出任何其他任務。這些任務包括運行靜態分析和程式碼格式檢查(有關完整詳細資訊,請參閱Travis 文件)。

以下是運行靜態分析的方法,它會檢查您的程式碼中是否存在分析錯誤:

1
2
3
language: dart
dart_task:
- dartanalyzer

以下是確保所有 Dart 檔案都已正確格式化的方法:

1
2
3
language: dart
dart_task:
- dartfmt

如果您有一個包含多個 Dart 套件的更複雜的 repo,則需要更詳細的設定。mono_repo 工具中的 travis 命令是建立此設定的一種可能的解決方案。

Travis CI 的替代方案

Travis CI 是持續整合的幾個熱門供應商之一。其他支援 Dart SDK 的供應商包括 AppVeyor範例)和 Codeship。對於 Flutter 應用程式,選項包括 NeverCodeCirrusCIBitrise

目前就這樣。我們希望您喜歡 Dart 的這個新的 CI 支援。


宣布 Travis CI 上 Dart 的 Windows 支援 最初發佈於 Medium 上的 Dart,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

讓 Dart 成為更好的 UI 語言

Dart 團隊中,我們正忙於實作一些讓我非常興奮的語言變更。它們都與集合字面量有關,即用於建立列表、映射和集合的內建語法:

如果您今天沒有編寫 Dart 程式碼,這可能與您和您的人生目標沒有太大關係,但我還是希望您能繼續閱讀。我認為這些功能本身就很有趣,而且它們背後的執行模型可能會以有用和/或引人入勝的方式拓展您的大腦。我總是覺得學習新的語言知識很有趣,即使是用我目前沒有使用的語言。

Flutter 使用者如何建構他們的 UI

如果您在過去一年中聽說過 Dart,那可能是在 Flutter 的上下文中。如果您沒聽說過這個名字,Flutter 是一個用於建構跨平台行動應用程式的 UI 架構。我無法在這裡詳細介紹它,但點選連結,它會回答您心中的每一個問題。(好吧,至少是關於 Flutter 的每一個問題。它不會告訴您為什麼您高中時暗戀的人從未回電。)

任何 UI 架構都會做出的一個關鍵選擇是如何定義基本的視覺化 UI 元素 - 按鈕、顏色、文字、佈局等。您是在某種單獨的「模板」或「標記」格式中編寫這些內容,還是在定義 UI 行為的可執行程式碼中編寫這些內容?每十五年左右,業界就會對哪個答案是正確的進行一次翻轉。

Angular 和大多數 Web 架構都遵循 HTML 的腳步並使用模板。React 將 UI 放入您的 JavaScript 中,但也新增了一個名為 JSX 的嵌入式 DSL,使其看起來像 HTML。我猜,試圖魚與熊掌兼得,儘管並非每個人都會將 HTML 描述為特別像甜點。

Flutter 使用普通的 Dart 表達式語法將 UI 直接放入您的 Dart 程式碼中。請看:

在 return 關鍵字之後的所有內容都是一個大的巢狀表達式,它會產生一部分使用者介面。使用 Dart 執行此操作有一些實質上的好處:

  • 只需學習一種語言:Dart。 由於 Dart 的設計讓來自其他語言的人感到熟悉,因此希望不會太難。
  • 在建構 UI 時,您可以使用通用程式語言的所有抽象功能。 將片段提取到可重複使用的函數中。為這些函數提供參數以改變生成的 UI。將內容儲存在局部變數中。隨心所欲。
  • 您永遠不會遇到表達能力的限制,而不必移植到其他語言。 如果您曾經使用過宣告式語言,您可能遇到過這樣的情況:您達到了它實際可以表達的極限。此時,您要么放棄您正在嘗試做的事情,要么費力地用低階的、通常是指令式語言重寫整個內容。由於您已經 使用具有完整功能的 Dart 語言中,因此您永遠不會遇到這種限制,並且您的 UI 程式碼可以順利地變得更加複雜。

當然,主要的挑戰,也是人們一開始就使用宣告式語言的原因,是用指令式語言定義內容可能非常繁瑣且難以閱讀。

想像一下,如果不是這一點 HTML:

您必須編寫如下內容:

幸運的是,現代語言和 API 並不 那麼 低階。即使語句是指令式的,表達式 也是相當宣告式的。雖然上面的程式碼很糟糕,但這段程式碼與 HTML 差不多:

現代的反應式範式,您透過從頭開始建構 UI 作為單個表達式,「建構」您的 UI,可以讓您走得很遠。上面 Flutter 範例中的相關部分只是:

它有括弧和方括弧而不是尖括弧,但除此之外與「標記」語言相距不遠。令人驚訝的是,這效果非常好。Dart 的語法基於 JavaScript,而 JavaScript 來自 Java,Java 來自 C。在此過程中,我們加入了方括弧列表字面量語法和命名參數,但這些都是相當 次要的

C 語言的設計目的是在 PDP-11 上實作命令列作業系統。它的符號在行動裝置上建構圖形化 UI 時的縮放比例並不太差,這既證明了 Ritchie 的設計品味,也證明了我們對 C 語法共同的斯德哥爾摩症候群。無論如何,它 大多數情況下 都有效。

在這裡的範例中,建構 UI 不需要任何有趣的執行時 邏輯。所有內容都很好地放入單個巢狀表達式中。但是假設,由於某些原因,您不想在星期二顯示文字的「這是 Flutter」部分。(也許您需要在螢幕上騰出空間來顯示「Taco Tuesday!」橫幅。)

有幾種方法可以表達這一點,但沒有一種方法像上面的範例那樣好和宣告式。以下是一種方法:

我們更接近於驅使人們使用模板的令人討厭的低階指令式程式碼。當我們查看真實的 Flutter 程式碼時,我們很遺憾地看到很多看起來像這樣的程式碼。因此,大約一年前,Flutter 團隊要求我們在 Dart 上提出語言變更,以使用 Dart 編寫的 UI 程式碼更容易編寫、閱讀和維護。

「UI 作為程式碼」

我們將此倡議稱為「UI 作為程式碼」,因為它是關於使用程式碼建構 UI。但最終目標是語言功能盡可能普遍適用於盡可能多的 Dart 程式,無論是否使用 Flutter。(如果您想了解更多背景資訊,這裡有一份我寫的 長篇動機文件。)

在探索了 許多選項 之後,我們決定專注於圍繞集合字面量的一些有針對性的改進。這看起來可能不像將 JSX 之類的東西塞進 Dart 中那麼性感(並不是說我完全排除這種可能性),但它的優點是使用者可以更容易地在他們的程式碼中逐步利用它。

僅僅讓列表字面量變得更有趣,似乎影響有限。但是,如果您查看上面的 Flutter UI 程式碼,它基本上是一個由建構函數調用和列表字面量組成的大樹。列表字面量佔據了很大一部分。(哎呀,整個語言 都是圍繞它們設計的。)如果您深入研究一些例子,在這些例子中,您感覺應該能夠以宣告式的方式編寫某些內容,但卻不得不進行一堆令人討厭的指令式修改,通常是圍繞 列表 的。

如果我們可以讓集合變得更好,我們就可以讓 很多 Dart 程式碼變得更好。為此,我們正在新增三個新功能:

展開運算符

通常,當您建構 Widget 列表時,其中一些 Widget 已經在其他 列表 中。以下是一些 Flutter 程式碼:

buildTab2Conversation() 方法返回一個 Widget 列表,我們希望用標題和頁尾將其圍繞起來。必須以指令式的方式建構結果列表真的很麻煩。它強制程式碼「反向」閱讀,在您看到一堆東西在 children 上亂搞之前,您必須先查看程式碼,才能看到它們是 誰的 子級。

Dart 有一個稱為 方法級聯 的功能,它可以有所幫助。這些功能讓您可以在表達式中間塞入一個修改方法調用,同時產生原始物件。有了這個,您會得到:

這有點好,但仍然很尷尬。那個尾隨的 ..add() 用於附加單個項目,尤其令人震驚。您可能已經猜到我們是如何解決這個問題的,因為許多其他語言已經有了相同的解決方案。(>90% 的語言設計是找出要從其他語言中借用哪些功能。)我們正在新增一個稱為 展開運算符新語法

在集合字面量中,展開運算符會解壓縮另一個集合並將其內容直接插入到位。例如:

列表元素之前的 ... 會導致其元素插入到周圍的列表中。這與 JavaScript 使用的語法相同。Python、Ruby 和其他一些語言使用前綴 * 來表示相同的意思,但我們認為它在視覺上不夠突出。有了這個功能,Flutter 範例變成了:

我相信這是一個真正的改進。列表視圖的所有子級都緊密地嵌套在一個列表字面量中。這看起來更好,並且與類型推斷也更好。有了列表中的所有元素,我們可以在推斷列表的 類型 時使用所有元素。

我這裡展示了一個 Flutter 範例,但我花了很長時間梳理了大量的 Dart 程式碼,以查看這種語法在哪些地方有用,它在所有地方都發揮了作用。特別是,用於調用其他程式的命令列參數列表的程式碼確實受益於展開運算符。

元素

在我介紹最後兩個功能之前,我想深入探討一下展開運算符實際上 是什麼。看起來我好像在過分強調這一點,但我保證清楚這一點在以後會有幫助。以下是一個主要問題:展開運算符是一個表達式嗎?

它看起來像一個表達式,因為它出現在列表字面量中,在預期表達式的地方:

像表達式一樣,您對它進行計算,它會產生一些資料。也許它是一個計算結果為 Iterable 物件的表達式?但是,等等,這說不通。這就是展開運算符 內部 的表達式所做的。如果您只想使用一個計算結果為 Iterable 的表達式,則無需在其前面加上 ...

展開運算符不會計算結果為單個 Iterable 物件,它會 解壓縮 該物件並計算結果為 Iterable 產生的 一系列 物件。然後將其重新打包回某個新物件是沒有用的。但是表達式總是計算結果為 單個 物件。

如果展開運算符是一個表達式,那麼在允許表達式的其他地方使用它意味著什麼?

這會做什麼?將整個 Iterable 作為物件儲存在 wat 中是沒有意義的。如果您想要這樣做,您可以完全省略 ...。答案是展開運算符 不是 表達式。它們是另一種語法類別。Dart 像許多語言一樣,已經有兩個大的語法組:語句和表達式。

語句會執行,但不產生任何結果值。相反,它們預期會產生一些有用的副作用。它們不能在任何需要值的地方使用,因為它不會給您提供值。這就是為什麼,例如,這是禁止的:

for 語句不會產生值,因此將其塞入變數初始化程式中是沒有意義的。確實有 將表達式和語句統一起來 並允許此類程式碼的語言。它們定義每個語句以某種方式執行, 產生一個值。但 Dart 不是這些語言之一。

表達式計算結果為單個結果值。您可以在值有用的地方使用它們。還有「表達式語句」 - 後跟分號的表達式 - 它們是包含單個表達式的語句。這很方便,因為許多表達式也恰好具有副作用,即使不需要它們的結果也很有用。

展開運算符不是其中任何一個。展開運算符可以計算結果為零個值(如果您展開空集合)、一個值或多個值。它是它自己的東西。這個類別的一個好名字是「生成器」。我的模型來自 Icon,其中 每個 表達式都可以是一個生成器。但 Dart 已經有了 生成器函數,所以我不想過度使用這個術語。

展開運算符只能出現在可以優雅地處理接收零個或多個值的地方。如果沒有徹底修改語言的執行模型並將其變成 Icon(我發現這很奇怪地吸引人,但可能不切實際……),那麼沒有太多地方符合這個限制。基本上是集合字面量,也許還有位置參數列表。(我為後者寫了一份 提案,但它相當複雜,所以我們沒有這樣做,至少現在沒有。)

這就留下了集合字面量的正文內部。基於此,我將這些稱為「元素」。元素是一段程式碼,當計算時會產生零個或多個值。然後,這些值會插入到它們出現的周圍上下文中。因此,在列表中,它們成為新列表中的一系列元素。在映射中,它們成為一系列鍵/值對。您明白了。

因此,集合字面量的正文可以包含表達式或元素。允許兩個類別有點令人困惑,但幸運的是,我們可以透過說集合只包含元素來簡化這一點。然後我們將「表達式元素」定義為包含單個表達式的元素。該元素總是產生一個結果 - 表達式的值。有點像表達式語句的元素等價物。

好的,這就是我們現在的處境。我們已經將集合更改為包含元素而不是表達式,並定義了兩種元素,展開運算符和表達式元素。要計算集合字面量,您可以遍歷元素,計算每個元素,並將所有結果物件連接起來(或在集合的情況下進行聯合)。

考慮到這個模型,我們可以瀏覽其他兩個新功能:

集合 If

編寫任何 Flutter 程式碼,您很快就會遇到這樣的情況:您想要建構的 Widget 樹會根據某些條件而變化。假設我們有:

後來,我們決定在 Android 上使用不同的搜尋按鈕。您已經可以使用條件表達式執行此操作:

這可以,但我從未覺得 C 的條件運算符很容易閱讀。但是,通常情況下,您不想根據條件 交換 Widget,您只想 省略 一個 Widget。假設您根本不想在 Android 上顯示搜尋框。今天,Flutter 使用者傾向於使用以下兩種模式之一。這是一種:

它強制您重新排列整個函數,方法是在使用子列表之前將其提取出來並以指令式的方式建構它。另一種模式如下:

這使用了一個條件表達式,它有時會產生一個 null,然後從結果列表中過濾掉該 null。向提出這個想法的人致敬,但這不是任何使用者為了完成如此簡單的任務而應該編寫的內容。簡單的問題應該有簡單的解決方案,對程式的小概念變化不應該需要大的文字變化。

以下是新的內容。對於我們想要在 Android 上使用不同按鈕的第一個範例,它看起來像這樣:

它使用熟悉的 ifelse 語法,而不是 ?:。這實際上與現有的條件表達式只差幾個標記,因此它似乎沒有發揮作用。更有趣的情況是當我們想在 Android 上 省略 按鈕時:

請注意,沒有 else 子句。這兩個範例看起來與 Ruby 等語言非常相似,在 Ruby 中,if 是一個表達式。但表達式必須始終計算結果為一個值,即使條件為 false。在 Ruby 中,在這種情況下,它隱式計算結果為 nil

但這 不是 您在這裡想要的。您不希望在子 Widget 列表中以 null 元素結束。這就是為什麼上面的條件表達式範例必須使用煩人的 where() 將其過濾掉。幸運的是,這在這裡不是問題。因為集合中的 if 不是 表達式。它是一個 元素

現在您明白為什麼我要帶您瀏覽所有關於展開運算符的東西了。元素為我們提供了基礎,讓我們可以使用 if 語法從集合中完全省略一個元素。如果條件為 true 或存在 else 情況,則 if 元素會產生單個值。如果條件為 false 且沒有 else 子句,則它根本不產生任何值。

我認為這種行為非常有用,但如果您查看程式碼並期望 if 的行為像一個簡單的表達式,也會令人困惑。

集合 For

前一個功能採用了現有的 Dart 語句語法,並將其重新用於在集合的上下文中執行一些有用的操作。還有其他值得採用的語句形式嗎?

大多數語句形式都沒有意義。在其中插入 return 語句不會做任何有用的事情,因為它只會退出周圍的函數。while 也不是很有用。為了退出 while 迴圈,主體通常包含 breakreturn 或某種副作用,例如指派。但這意味著主體包含 語句,這不是我們想要的。目標是使集合更具 表達性,而不是更具 指令性

我仔細研究了現有程式碼中的許多集合字面量,尋找我認為可以透過新語法改進的模式。到目前為止,最主要的是 if。但我看到一些地方我認為可以透過 for 改進。以下是我找到的一些程式碼的略微清理後的範例:

所有 command.add() 的東西都感覺不必要地指令式。如果我們允許在集合字面量中使用 for 迴圈,則變為:

組合元素

鑒於我們已經加入了展開運算符,for 語法似乎並沒有那麼引人注目。您不能使用展開運算符與可迭代物件上的高階方法的某種組合來完成同樣的事情嗎?是的,您可以。您會得到如下內容:

這確實有效,並且適用於某些使用案例。讓我們考慮一個稍微不同的例子。假設我們只想在存在相應的 JSON 檔案時包含一個入口點。這意味著我們沒有進行簡單的 1-1 對應。僅使用展開運算符,我們會得到如下內容:

這也有效。但將簡單的「如果檔案存在,則執行此操作」邏輯轉換為基於流的高階函數樣式變得越來越困難。總有一些 map()where()transform() 的組合可以完成這項工作,但感覺就像將俳句翻譯成逆波蘭表示法。

有一個更乾淨的解決方案,它涉及一個關鍵問題:這些新的 iffor 元素的 主體 是什麼?在我目前向您展示的範例中,它始終是一個表達式。但沒有必要僅限於此。相反,我們允許任何元素都放在那裡。換句話說,所有這三個新功能都可以自由組合。上面的程式碼可以表示為:

for 內部的一個簡單的 if,就像您在編寫指令式語句時所做的一樣。組合元素的語義非常明顯:

  • 如果條件為 true,則 if 元素會產生其 then 子句產生的所有值,否則會產生「else」子句的所有元素。如果沒有「else」,則不產生任何元素。
  • 每次執行主體時,for 元素都會產生其主體元素產生的所有值的連接。

這可以啟用一些我認為很酷的模式。您遇到的顯顯問題是想根據單個條件包含或省略 多個 值。因此,假設在我們之前的範例中,我們想跳過 Android 上的標題和搜尋框。您可以透過將展開運算符包裝在 if 中來執行此操作:

這裡需要展開運算符來解壓縮內部列表。否則,當不在 Android 上時,您將包含整個內部列表作為單個值。(我們考慮過在這種情況下根據靜態類型隱式展平,但當您考慮 List<Object> 之類的東西應該如何表現時,這就變得非常可疑了。)

您可以將展開列表字面量視為語句的元素等價物 - 它讓您可以在只預期一個元素的地方放置多個元素。(如果您熟悉 逗號運算符,那基本上就是表達式的類似形式。到處都是類比。)

在空集合字面量中使用 forif 可以讓您獲得與其他語言(如 Python)支援的特殊「列表推導式」語法不太一樣的語法:

您甚至可以巢狀 for

這會建構一個列表,其中包含給定 horvert 矩形中所有點的 笛卡爾積

此外,這些新功能可以跨集合類型組合。我一直使用列表作為範例,因為它們最常出現,但所有這些功能也適用於映射和集合。唯一的區別是,對於集合,重複項會被隱式丟棄。在映射中,基本元素不是原始表達式元素,而是鍵/值對。例如,這:

可以重寫為:

我們對所有這些功能的一個真正擔憂是,我們基本上是在為您提供新的方式來表達您今天已經可以表達的東西。這是有代價的,因為這意味著使用者需要花費腦力來 決定使用哪個功能,並且在閱讀其他人的程式碼時,他們可能會花時間質疑為什麼選擇一個選項而不是另一個選項。只需學習更多功能,語言就更大了。

我們花了很長時間 為此而苦惱。有時候,什麼都不做是最好的設計。簡單性非常有價值,而且您很少有機會讓一種語言隨著時間的推移變得更簡單。但是,在查看了大量程式碼並與一位令人愉快的 UX 研究人員合作進行了一項研究後,我們相當有信心這些功能足夠輕量級且有用,可以發揮其作用。

與任何語言更改一樣,在使用者使用之前,您永遠不知道它會如何運作。這些功能將在即將發佈的 Dart 2.3 版本中提供,我非常期待看到您如何使用它們。


讓 Dart 成為更好的 UI 語言 最初發佈在 Medium 的 Dart 上,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

宣佈 Dart 2.2:更快的原生程式碼,支援集合字面量

今天,我們宣佈推出 Dart 2.2 SDK 的穩定版本,這是 Dart 2 的一個增量更新,它提供了改進的 提前 (AOT) 編譯 原生程式碼的效能和一個新的集合字面量語言功能。

針對 Flutter 開發人員的 Dart 效能改進

我們繼續努力使 AOT 編譯的程式碼(例如 Flutter 應用程式)更快。在 Dart 2.1 中,我們減少了類型檢查的開銷,大大降低了 AOT 編譯程式碼和使用 JIT(即時)編譯在 VM 中執行的程式碼的類型檢查成本。

在 Dart 2.2 中,我們特別關注 AOT 編譯程式碼的效能,在微基準測試中將 AOT 效能提高了 11-16%(代價是程式碼大小增加了約 1%)。這一改進是幾個季度以來努力減少靜態調用開銷的結果。我們最佳化的 AOT 程式碼現在能夠使用 PC 相對調用(即使用 程式計數器)直接調用目標;之前我們必須對物件池進行多次查找才能確定目標地址。當程式碼包含大量建構函式和靜態方法調用時,這些最佳化特別有用,例如建立大量 Widget 的 Flutter 使用者介面程式碼。

Dart 2.2 集合字面量語言功能

Dart 的核心函式庫 (dart:core) 包含許多集合類別,用於對物件的 映射列表集合 進行建模。映射是鍵值對的集合。列表是有序的值序列,其中每個值可以透過索引存取,並且可以出現多次。集合是無序的值集合,其中每個值只能出現一次,並且可以有效地檢查集合中是否存在值。

Dart 集合通常使用編譯時常數初始化,因此 Dart 有一種方便的字面量語法來表達這種初始化。在 Dart 中,可以使用以下程式碼初始化列表:

1
var list = ['USD', 'EUR', 'JPY'];

以前,Dart 僅支援列表和映射的字面量語法,因此集合的初始化很麻煩,因為我們必須透過列表進行初始化:

1
var currencies = new Set.from(['USD', 'EUR', 'JPY']);

這段程式碼不僅不方便且效率低下;缺乏字面量支援會導致貨幣無法成為編譯時常數。透過 Dart 2.2 將字面量擴展到支援集合,我們可以使用一種方便的新語法初始化集合並使其成為常數:

1
const currencies = {'USD', 'EUR', 'JPY'};

有關 Flutter 團隊如何開始應用集合字面量的真實範例,請參閱 PR #27811。有關如何使用集合字面量的更多一般詳細資訊,請參閱 更新的 Dart 2.2 語言導覽

使用 Dart 2 通用前端 (CFE) 建立新的語言功能

Dart 語言實作(Flutter 使用的 Dart VM、dart2js 編譯器和 Dart 開發編譯器 (dartdevc))共用一個通用前端。Dart 通用前端(或 CFE)會解析 Dart 程式碼、執行類型推斷,並將 Dart 轉換為後端實作接受為輸入的低階中間語言。

集合字面量功能是一個語言結構的例子,由於 CFE,我們能夠快速開發它。解析集合字面量和執行類型推斷的程式碼在 CFE 中為所有 Dart 後端實作一次。此外,我們建立了一個僅限前端的過渡實作,後端最初可以使用它。在過渡實作中,上述非 const 版本的貨幣集合字面量在編譯期間被轉換為等效的:

1
2
3
4
var currencies = new Set<String>();
currencies.add('USD');
currencies.add('EUR');
currencies.add('JPY');

const 集合字面量的過渡實作是不同的,因為 const 集合不能以片段的形式逐步建立。相反,我們根據一個私有的不可修改集合類別來實作它,該類別包裝一個 const 映射,其中集合元素是映射的鍵:

1
const currencies = _UnmodifiableSet({'USD': true, 'EUR': true, 'JPY': true});

不可修改集合類別根據包裝的映射實作 Set 介面中的方法。

總體而言,我們最初能夠將集合字面量實作為僅限 CFE 的功能。後端最初可以使用 CFE 實作,然後獨立於功能的初始啟動開發自己的原生支援。這允許後端推遲其原生支援,直到更好地理解此功能的效能方面。

Dart 2.2 語言規範

Dart 2 是 Dart 的一次 重大升級,我們花了一段時間才將正式的 Dart 語言規範 更新以符合我們所做的所有變更。我們終於完成了這項工作,並且規範已更新至 Dart 2.2。我們還將語言規範來源移至新的 語言儲存庫,並加入了持續整合,以確保在我們為未來版本的 Dart 語言發展規範時,以 PDF 格式生成滾動草案規範。2.2 和滾動 Dart 2.x 規範都可以從 Dart 規範頁面 獲得。

獲取 Dart 2.2

Dart SDK 2.2 現在可以從 Dart 首頁下載。如果您是 Flutter 開發人員,則 Dart 2.2 已包含在內。(注意:目前的 Flutter master 和 dev channel 將報告 Dart 2.2。今天的 Flutter 1.2 穩定版本將報告 Dart 2.1.2;這與 Dart 2.2 具有相同的功能)。

目前就這些。我們希望您喜歡 Dart 2.2!


宣佈 Dart 2.2:更快的原生程式碼,支援集合字面量 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

宣佈 Dart 2.1:改進效能和易用性

今天,我們宣佈推出 Dart 2.1 穩定版,這是 Dart 2 的一個更新,它提供了更小的程式碼大小、更快的類型檢查、更好的類型錯誤易用性,以及新的語言特性,以提高構建使用者體驗時的生產力。

Dart 2 正式發佈

Dart 2 是 Dart 平台的一次重大升級。它完成了從早期網頁根源到主流程式語言的轉變,適用於跨行動裝置和網頁快速開發豐富的使用者體驗。Dart 作為一種語言,在提供虛擬機器 (VM) 和編譯成原生機器碼和 JavaScript 方面都有些獨特。這支援了各種平台的不同需求,無論是開發還是生產。

作為向 Dart 2 過渡的一部分,我們加入了健全的類型系統,以支援大型團隊構建複雜的應用程式;新的編譯器支援生成針對行動裝置最佳化的原生程式碼;以及完全重新設計的網頁平台工具。Dart 為 Flutter 提供動力,Flutter 是一個快速發展的工具包,用於從單一程式碼庫為 iOS 和 Android 構建美觀的原生體驗;它也是 Google 一些最大專案(例如 Google Ads)使用的語言。

自 8 月份 Dart 2 發佈 以來,我們一直致力於讓整個生態系統都使用 Dart 2。我們將 dartlang.org 網站切換到使用 Dart 2 範例,並提供用於遷移 Dart 1.x 套件的工具和文件。我們理解 Dart 2 中的重大變更涉及到現有 Dart 開發人員的遷移工作,我們非常感謝那些幫助我們為未來建立新基礎的人。我們現在的重點轉向利用這些投資來改進效能和生產力。

Dart 2.1 語言支援 int 到 double 的轉換

新的 Flutter 開發人員經常在指定 padding、設定字體大小等時遇到如下分析錯誤:

從系統的角度來看,這些錯誤是有道理的:API 需要一種類型 (double),而開發人員指定了不同類型 (int) 的值。但是,從易用性的角度來看,這似乎有點愚蠢:從 int 到 double 有一個簡單的轉換,為什麼不直接這樣做呢?Dart 2.1 現在可以推斷 int 何時可以靜默地評估為 double

Dart 2.1 語言支援 Mixins

我們還改進了 Dart 對 Mixins 的支援。如果您以前沒有接觸過 Dart Mixins,那麼值得閱讀 Romain Rastel 撰寫的這篇關於 Dart Mixins 的精彩介紹。Dart 2.1 引入了一種新的 Mixins 語法,其中包含一個新的 mixin 關鍵字,您可以使用它來定義只能用作 Mixins 的類別。我們還加入了支援,以便 Mixins 可以擴展其他類別(以前它們只能擴展 Object),並且可以在其父類別中調用方法。

擴展非 Object 類別的一個例子來自 Flutter 的動畫 API,其中 SingleTickerProviderStateMixin(一個提供用於將動畫前進一幀的 ticker 的框架類別)宣告了一個實作通用 TickerProvider 介面的 Mixin。動畫僅適用於有狀態的 Widget(因為動畫中的位置被視為狀態)。新的 Mixin 支援允許我們透過宣告只有擴展 Flutter State 類別的類別才能使用 Mixin 來表達這一點:

Dart 2.1 編譯時類型檢查

Dart 2 的健全類型系統可以在開發過程中保護您,當您違反類型指定的契約時,它會告訴您。例如,假設您正在為 Flutter 建立一個狀態類別。這個類別應該擴展框架 State 類別。State 類別應該傳遞包含其狀態的 StatefulWidget。

現在,如果您犯了一個程式設計錯誤,並且例如傳遞了 StatelessWidget 的後代而不是 StatefulWidget,類型資訊可以使工具捕獲錯誤並立即向您顯示警告:

這些編輯時類型檢查已在 Dart 2.0 中加入(由 Dart Analyzer 提供支援)。但是,還有另一個地方您可能會期望進行這些類型檢查,即在編譯時,當您進行 Flutter 發佈構建時。這些檢查在 Dart 2.0 中是不完整的,這可能會導致易用性問題,錯誤的原始程式碼可以在不產生任何錯誤的情況下進行編譯。在 Dart 2.1 中,這些檢查是完整的,並且 Analyzer 和 Dart 編譯器包含相同的檢查。

Dart 2.1 針對 Flutter 開發人員的效能改進

對於在我們的 VM 上執行的 AOT 編譯程式碼(例如 Flutter 應用程式),Dart 2 通常比 Dart 1.x 更快。但是,在一些邊緣情況下,新類型系統 加入的全面檢查會導致 20-40% 的額外開銷。在 Dart 2.1 中,我們大幅降低了類型檢查的成本,無論是對於 AOT 編譯程式碼,還是對於使用 JIT(即時)編譯在 VM 中執行的程式碼。

一個由此受益的具體案例是我們的開發人員工具(使用 VM 執行)。例如,對一個大型基準測試應用程式(基於 Flutter Gallery 的多個串聯副本)執行程式碼分析過去需要約 41 秒;現在只需要約 25 秒。

Dart 2.1 針對網頁開發人員的效能改進

我們還改進了在網頁上執行的 Dart 程式碼的程式碼大小和編譯時間。我們專注於 dart2js 的輸出大小,並且看到了良好的結果,例如,對於我們的一個範例,縮小後的輸出大小減少了 17%,編譯時間提高了 15%

核心 SDK 之外的變更

除了 Dart SDK 中的上述變更之外,我們還在核心 SDK 之外進行了一些令人興奮的變更。

Protocol Buffers(簡稱 protobuf)是一種平台中立的序列化結構化資料的機制。它們在 Google 內部廣泛使用,並且在 Google 之外也得到了廣泛的採用,例如作為 gRPC 的一部分。Dart 現在是一種官方支援的 protobuf 語言,我們在 核心 protocol buffers 網站 上加入了詳細的文件,其中包含教學參考文件。

您可能聽說過 knative,這是一個基於 Kubernetes 的平台,用於支援構建、部署和管理無伺服器工作負載。我們最近調查了在 knative 上支援 Dart 程式碼的服務,並建立了一個小範例。對於大多數 Dart 應用程式開發人員來說,這可能太低階了,但我們從平台的角度來看,覺得這非常令人興奮,並預計這將是改進 Google Cloud 中 Dart 程式碼服務支援的關鍵基石,例如為 Flutter 應用程式建立後端。

獲取 Dart 2.1

Dart SDK 2.1 可以從 Dart 首頁下載。如果您是 Flutter 開發人員,Dart 2.1 將作為即將推出的 Flutter 1.0 版本的一部分包含在內。

我們還更新了 DartPad 以執行 Dart 2.1,並加入了 int-to-doublemixins 的範例。

接下來是什麼

我們預計將發佈多個 2.x 版本,以利用 Dart 2 平台基礎。我們將根據框架合作夥伴和應用程式開發人員的需求來制定這些版本。在 2019 年,我們預計將研究以下領域:

  • Dart 語言的持續發展:對於 Dart 2.2,我們正在研究許多變更,例如對 const 表達式 的改進和對新的 Set 字面量 的支援。除了 2.2 之外,我們還在研究對 Unicode 字串的支援,並且正在研究是否可以提供更好的空值安全性。
  • 進一步改進以最佳化 Dart 作為建立使用者介面的語言:Dart 2 以其新的類型系統和可選的 new 開始了這段旅程,Dart 2.1 加入 int 到 double 值推斷,我們目前正在研究許多潛在的改進,例如支援 Widget 列表中的條件,將 物件集合 擴展到其他集合,甚至可能移除使用分號終止語句的需要
  • 更好的效能:我們將繼續減小編譯後 Dart 程式碼的大小和提高效能,包括更好地支援使用多核處理器,進一步減小程式碼大小以改善下載和啟動時間,以及可能更好地控制大小或運行時效能是否最重要。

要了解更多關於 Dart 和 Flutter 的資訊,我們誠邀您加入我們參加 Flutter Live,這是一個線上活動,將於 12 月 4 日舉行,並將在網路上進行直播。我們很興奮屆時能與您分享更多關於路線圖的資訊。

目前就這些。我們希望您喜歡 Dart 2.1!


宣佈 Dart 2.1:改進效能和易用性 最初發佈於 Medium 上的 Dart,人們在那裡透過突出顯示和回應這個故事來繼續對話。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Future 與 Future,有什麼區別?

在 Dart 2 的諸多升級中(除了更好的靜態檢查、運行時類型安全、可選的 new/const、核心函式庫改進等等),一個值得一提的改進是 void 的正式化,使其更加實用且更不容易出錯。這在異步程式設計中尤其清晰,您可以為異步函數編寫 Future<void>,這些函數在工作完成時不返回結果。在此之前,您可能使用了 Future<Null>,所以我們經常被問到的問題是:Future<void>Future<Null> 之間有什麼區別?我應該使用哪一個,以及何時使用?

由於我的目標是提供有用的資訊,我將從 TLDR 開始。

TL;DR:99.99% 的情況下,您應該使用 void 類型。

我還建議您現在就在您的專案中啟用兩個與 void 相關的lint 規則

  • prefer_void_to_null: 幫助您改掉輸入幾乎過時的 Null 的習慣。
  • void_checks: 為您提供可能更直觀的 void 語義,即使它們對於安全程式碼並非絕對必要。

本文的其餘部分並非輕鬆閱讀。它結合了歷史、邊緣案例和類型理論。我接下來要寫的就像試圖解釋單子,但我會盡力而為,希望您能跟上。

快速 Future 小知識

為了展示閱讀本文的價值,我為您提出了一個小知識問題。

假設我們將自己限制在兩個 Future - 一個 Future<User> 和一個 Future<Null>。以下程式碼行會做什麼?

1
2
// 這段程式碼在您的編輯器中會失敗嗎?
Future<User> f1 = Future<Null>.value(null);
1
2
// 這段程式碼在運行時會失敗嗎?
Future<Null> f2 = Future<User>.value(0);

如果我們改用 await 呢?

1
2
User u = await Future<Null>.value(null);
Null n = await Future<User>.value(0);

暫停以產生戲劇性效果

答案是,除非您禁用了隱式向下轉型,否則這四行程式碼都不會在您的編輯器中顯示任何錯誤。沒錯,使用 Future<Null> 並假裝它是它不是的東西,例如 Future<User>,是 100% 合法的。Future<void> 就不是這樣!

同樣不直觀的是,第二行和第四行在運行時會失敗(以這種方式丟棄 Future 的結果是不「安全」的),而其他行則靜默地成功。

這些行為應該會讓您感到不安。但別擔心!您已經知道解決方案:使用 Future<void>。我將分析為什麼這種不直觀的行為會這樣工作。

void 現在是如何工作的

我一直在思考本文的最佳順序。這裡有很多概念需要解釋。但是,我會盡量按照從最有用的資訊到最不有用的資訊的順序進行。因此,由於 void 在 Dart 2 中比 Null 更有用,因此從 Dart 2 void 語義開始是有意義的。

Dart 中 void 背後的基本思想源於 void 值不應使用的目標。

1
2
void f() {}
print(f()); // 錯誤!

希望這就是您期望發生的!到目前為止,一切都很好。

值得注意的是,實現這個目標並且將 void 作為一個相當普通的類型公開會有點奇怪:

1
2
3
4
f(void argument) {
print(argument); // 錯誤!
// 我們可以接收參數,但我們不能使用它!
}

這就是 Dart 2 中我們新的「廣義 void」開始變得酷炫和強大的地方……儘管有時會讓人感到困惑。

我向您提出一個問題……我們可以傳遞什麼類型的值作為 f 的參數?

1
2
f(void argument) { … }
f(x); // x 是什麼?

答案是……任何東西!因為我們不允許您在函數 f 中使用 x 的值,所以我們可以自信地說 x 可以取的任何值都不會導致任何運行時錯誤。

1
2
3
4
f(1); // 與以下程式碼沒有區別
f("foo"); // 與以下程式碼沒有區別
f([1, 2, 3]); // 與以下程式碼沒有區別
// ...

如果我們根據 void 是一個不會被使用的值的思想推導出 void 的最佳語義,這就是我們得到的結果。這意味著,它是一個可以用任何東西填充的值。它就像一個真空:一個沒有輸出的輸入。

現在,在某些情況下,某些東西在運行時總是沒有錯誤,但仍然應該導致靜態錯誤。這是一個 lint 的絕佳案例!事實上,這就是 void_checks lint 背後的思想。它會尋找您將 null 以外的任何東西傳遞到 void 位置的地方,我鼓勵團隊啟用它。它並非健全性所必需的,但是將 null 以外的任何東西傳遞到 void 位置仍然可能是一個意外,lint 會為您標記它。

由於所有這些都基於基礎類型理論,因此 void 可以與 Dart 的每個部分都很好地配合使用,即使在利用類型推斷的 Future<void> 上下文中的延續也是如此:

1
2
3
4
Future<void>().then((x) {
print(x); // 錯誤!x 的類型為「void」,所以您不能列印它!
// 這就是我們想要的!
});
1
2
// 是的,這也是一個錯誤:
Foo f = await voidFuture();

此時,普通開發人員可能已經了解了有效使用 void 所需知道的一切。

然而,如果您仍在繼續閱讀,還有一些更有趣的東西。

即使啟用了 void_checks,也不能保證 void 位置包含 null。以下列覆寫為例:

1
2
3
4
5
6
7
8
class A {
void f(Object o) {}
}

class B extends A {
@override
Object f(Object o) => o;
}

我們不想讓此覆寫非法,因為它是安全的、有用的,並且與 Dart 1 相比是一個重大變更。因此,我們不得不接受 void 位置可能包含任何值。我們也不能「優化掉」A.f() 返回的值,因為在運行時它可能是一個 B!

相反,我們有一個聰明的選擇,即讓 void 成為 Object 的姊妹類型。畢竟,它可以包含任何值,並且所有值都是物件。這不是我們設計的東西,它只是現實。認識到現實,我們就可以利用它。

透過使 void 成為 Object 的同級,我們沒有任何要求 void 值完全未使用。我們盡力讓 void 保持自身,但為了向後兼容性,我們特意在一些地方放鬆了這些限制:

1
2
dynamic f() => voidFn(); // 這是合法的
voidFn() as dynamic; // 這是合法的

這些是對於 Object 合法的特殊情況,為了使 Dart 2 更順利地推出,我們也讓它對 void 合法。

使 void 成為 Object 的同級也意味著 void 可以用作類型參數,而不會使編譯後的輸出膨脹(對於 C++ 使用者來說,沒有「模板特化」)。這種減少有助於保持 Web 和 Flutter 應用程式的小巧,但代价是允許以下操作:

1
<void>[1, 2, 3].toString(); // 這是合法的,並列印 [1, 2, 3]

最後,將參數類型指定為 void 可能看起來沒有用,尤其是因為它是 Object 的一種形式。然而,void 值可以傳遞到 void 位置

1
2
f(void x) { … }
f(voidFn()); // 這是合法的

這對於排序很有用,例如在模擬返回 void 的方法時,Mockito 會使用它。(將上面的程式碼中的 f 替換為 when 以獲得更接近的近似值。)

總而言之:

  • void 類型是 Object 的同級。
  • 幾乎總是,void 物件不能被使用。
  • 標記為 void 的東西實際上可以是任何東西。
  • 任何東西都可以「丟棄」到標記為 void 的位置,而 lint void_checks 限制了這種行為。
  • void 值可以傳遞到其他 void 位置。

底部類型

在我開始討論 Null 之前,我們必須先討論「底部」類型。

這是一個類型理論中自然發生的類型,它有一個簡短的學術定義和一些實際應用。

如果您因為它太奇怪了而停止閱讀本節,那就是我最初 TLDR 的有力證據。除非您希望您的程式碼同樣奇怪,否則您可能需要 void。現在讓我們探索這個奇怪的兔子洞,看看它有多深,好嗎?

底部類型是所有類型的子類型。用更簡單的面向對象術語來說,這意味著它是一個人。還是一輛車。還是一種動物。還有來自每個程式的所有其他類型。

如果這聽起來很荒謬,那是因為它確實如此。我喜歡將它視為一個「佔位符」類型。但是「荒謬的」或「虛構的」類型也是一個合理的稱呼。然而,它被稱為底部類型,因為它是類型層次結構的底部,在電腦科學中,它是顛倒的。¯_(ツ)_/¯。正式地,它可以用符號 ⊥ 表示,您可能也認識到它是「假」的符號。

如果您試圖想像一個既是人又是又是動物的值,您將無法想到任何東西。令人驚訝的是,這就是 ⊥ 的實際用途的來源!

如果我編寫一個永不返回的函數會怎樣?有兩種簡單的方法可以做到這一點:

1
2
3
loopForever() {
while(true); // 第一種方法
}
1
2
3
alwaysThrow() {
throw Exception(); // 第二種方法
}

這兩個函數的最佳返回類型是什麼?

這取決於情況。因為函數永不返回,所以返回類型並不重要。您可以使用任何類型 - 即使是荒謬的底部類型,它可以在各種語言中以各種方式使用。

C++ 將其稱為 noreturn。Rust 有 !,Scala 有 Nothing。但我最喜歡的例子來自 Haskell,它有一個非常常用的 undefined 函數,它會中止程式。它返回,您猜對了,底部類型。

如果我們假設 Dart 中有一個 undefined 函數,就像 Haskell 中的那樣,它在被調用時只拋出一個異常並返回 ⊥,那麼它看起來像這樣:

1
Foo foo = cond ? val : undefined();

在示例用法行中,當 cond 為真時,程式可以安全地運行並儲存 val。而當 cond 為假時,程式將在 undefined() 期間拋出異常。將 undefined() 的結果「儲存」到 foo 中是安全的,無論 foo 的類型如何,因為該儲存永遠不會真正發生!

undefined() 這裡沒有返回任何東西。但這裡的教訓不是我們可以讓 foo 為空……而是底部類型像一個空的承諾一樣是空的。它比空還空。它永遠不會發生。

我必須小心地說明的一點是,您可以在實踐中從這些函數中返回 void,並且根據用法,通常應該返回 void。通常,像 return loopForever() 這樣的程式碼更有可能是一個錯誤,而不是一個有用的模式。然而,您可以自行選擇。

底部類型也適用於唯讀空列表。在 Dart 中,List 是協變的,因此允許將 List<int> 用作 List<Object>。如果您試圖將 String 放入該 List<Object> 中,運行時檢查會為您捕獲該錯誤並拋出錯誤。

這意味著,如果我們建立一個 ⊥ 列表,我們就無法在其中放入任何東西,但我們可以將其視為任何東西的列表:

1
2
3
4
List<int> intList = <⊥>[];
for (int i in intList) {
print(i * 2); // 有效,因為這永遠不會發生
}

當您查看所謂的底部類型的「逆變」位置時,還會有更多有趣的案例。

假設我們定義了一個具有 ⊥ 參數的函數:

1
void f(⊥ x) {}

這幾乎與 undefined() 示例相反。我們不是聲明了一個永不返回的函數,而是聲明了一個無法被調用的函數!沒有實際值可以賦值給該參數 x。您不能傳入 Person,因為它也不是 Car,並且您不能傳入 Car,因為它也不是 Person。您唯一可以傳入的就是荒謬的類型本身:

1
f(undefined());

但是正如我們之前所討論的,undefined() 永不返回,所以在這種情況下,f() 仍然永遠不會被實際調用!

將參數類型指定為 ⊥ 可能看起來沒有用,但它具有深奧的價值,因為所有可以被調用的函數都是不能被調用的函數的子類型。(想一想:一個可以被調用的函數不一定要被調用。如果一個函數沒有被調用,它就不會產生運行時錯誤。)

如果您還在繼續閱讀,請深呼吸,並拍拍自己的背。

具體來說,將任何 Function(X) 轉換為 Function(⊥) 是安全的,對於任何 X 都是如此。這比使用 Dart 的包羅萬象的 Function 類型更好,因為它更具體。

例如,您可以將任何一元函數儲存在一個欄位中,並動態調用它,以繞過靜態錯誤,如果您犯了錯誤,則用運行時錯誤代替它們:

1
2
Function(⊥) f;
f = (int x) => x + 1;
1
2
// 123 作為 f 的參數的有效性在運行時檢查
(f as dynamic)(123);

這是一個在緊要關頭幫助您的小技巧。

現在我們可以討論 Null 了。

Dart 2 中的 Null

概念上的「底部」類型(所有類型的子類型)存在於 Dart 中,但這並不是全部。這樣的值也存在於 Dart 2 中!在 Dart 中,我們稱它為 Null,而該值就是 - 您猜對了 - null。

由於我可以使任何東西都為 null(/Null),所以在 Dart 中荒謬的類型並不那麼荒謬。這使得它有點複雜。

注意:值 null 當然不是 Car,也不是 Person。我們確實收到了對 Dart 中非空類型的請求。因此,如果我們確實對 Dart 進行了這樣的更改,我們將需要一個新的底部類型,可能命名為 Nothing 之類的名稱,那時它將是一個更真實的底部類型。

Null 不僅具有與底部類型相同的所有荒謬用法,而且還有一個逃生出口。如果您真的需要從一個不能返回的函數中返回,或者調用一個不能被調用的函數,我們會給您一個出路!您可以將 null 傳遞進去!老實說,如果您從整體上看,我們不這樣做會有點不公平。

但是,對於一個原本簡單的聲明,它確實給出了很多警告:

1
Null nothing() => null;

Null 是 foo() 最特定的類型,所以它是一個合乎邏輯的選擇,也是一個良好的起點。如果您在其上調用不存在的方法,分析器也會善意地警告您:

1
nothing().x; // 錯誤!Null 上沒有成員 x

但这可能会造成安全的假象。

1
2
nothing().toString(); // 沒有錯誤:Null 定義了 toString()
Foo foo = nothing(); // 沒有錯誤:foo 將變為 null
1
2
3
// 對於 f(x) 中的所有參數類型都可接受,
// 如果 f(x) 需要非 null 值,則會出現運行時錯誤
f(nothing());

如果您的目標是讓 nothing() 成為

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Dart 套件網站的探索功能提升

七月份時,我們談論了 Dart 套件網站 (http://pub.dartlang.org) 的變更,其中包括更好地支援分析套件的潛在問題。從那時起,我們一直專注於進一步改進在套件網站上探索內容的功能。

Dart 2不相容性

絕大多數最新的熱門套件都已遷移以支援 Dart 2,因此它們已準備好用於FlutterAngularDart 5 應用程式。然而,有些套件(大多數是未維護的舊套件)仍然不支援 Dart 2。為了確保您可以輕鬆地發現這些套件,我們現在在總覽和套件詳細資訊頁面中加入了 Dart 2 不相容標籤。

與 Dart 2 不相容的套件會清楚地標示為「Dart 2 不相容」

此外,我們預計在未來版本的套件網站中會歸檔/停止使用這些不相容的套件,因此如果您是不相容套件的作者,請考慮盡快遷移!

支援核心函式庫

Dart 附帶了一組豐富的核心(標準)函式庫。開發人員告訴我們,很難記住特定的工具函式是在核心函式庫中還是在套件中。為了幫助您找到所需的 API,pub.dartlang.org 上的搜尋現在不僅包含已發佈的套件,還包含核心函式庫。來自核心函式庫的搜尋結果在評分圈中帶有 sdk,以及註釋 Dart 核心函式庫。如果您點擊該項目,您將直接進入函式庫 API 文件。

SDK 核心函式庫中的搜尋結果會包含在結果中

套件更新 Feed

Pub 已經提供 Atom Feed 一段時間了;為了讓它更容易被發現,我們在網站頁尾加入了一個直接連結。

評分變更

我們對健康和維護的評分模型進行了一些簡化和調整。有關目前模型的概述,請參閱 pana README(pana 是提供評分的工具)。

未來,我們計畫透過直接在套件分析頁面上顯示每個報告的問題降低了整體評分的程度來提高透明度。

歡迎提供回饋

發現問題了嗎?有什麼好主意或建議嗎?請透過在我們的追蹤器中開立 issue 來告訴我們。另請注意,即使是套件網站本身也是在 pub-dartlang-dart 儲存庫 中作為開源程式碼製作的,非常歡迎貢獻和想法!


Dart 套件網站的探索功能提升 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Dart 2:void 的遺產

Dart 2 中類似 void 類型宇宙的半精確描述

我在 StackOverflow、Gitter 甚至 Google 內部支援頻道上看到的最常被問到的問題之一是 Dart 2 中以下內建類型之間的差異:ObjectdynamicvoidNull

長話短說,Null(或其他語言中的 Bottom,即「無」)不應該在大部分真實使用者程式碼中使用,我懷疑在不久的將來我們會看到更多文章和 lint 來溫和地阻止使用。

其餘三個類型則不太清楚,因為在 Dart 2 中,任何東西在執行時都可以是 dynamicObjectvoid,僅根據 靜態 類型簽章而有所不同。因此,讓我們看看幾個 何時 應該使用哪種類型簽章的實際範例。

Object

Object 是 Dart 類別階層的根類別,Dart 中的每個其他類別都是 Object 的子類別——包括像 intdoublebool 這樣的「原始」類型。它保證了一些東西:一個 hashCode 屬性,一個 == 運算子,一個 toString 方法

實際上,我使用 Object 作為窮人的聯合類型——期望使用者在使用某個東西之前使用 is 運算子來確定它的真實類型。**我不使用 dynamic**,因為正如下一節所述,它會停用重要的靜態分析,並且更容易讓您進入無效狀態。

1
Object readProperty(String name) { ... }
1
2
3
4
5
6
7
8
void main() {
var age = readProperty('name');
if (age is int) {
print('I am $age years old');
} else if (age is String) {
print(age);
}
}

另一個選項是使用 Object 來宣告您 不關心 資料結構的內部類型是什麼,例如 List<Object> 可能表示「任何東西的列表」。例如,在編寫一個組合 List 中每個元素的 hashCode 的函式時,這就派上用場了:

1
int hashList(List<Object> elements) { ... }

Object 的一個很好的特性(與 dynamic 相比)是,如果您嘗試在其上調用一個不存在的方法,您將立即獲得分析和編譯器回饋。例如,以下程式碼會產生一個 靜態錯誤

1
2
3
4
void main() {
Object a = 5;
a.aMethodThatDoesNotExist();
}

然而,在實際應用中,Object 是相當(並且有意地)有限的。我希望 Dart 將獲得對方法重載的支援,這將允許我在真實程式碼中顯著減少 Object 類型的使用。

dynamic

我個人在 Dart 2 中 從不 使用 dynamic 類型。在我看來,它是 Object 和一個特殊指令的聯合,該指令告訴工具和編譯器 停用靜態分析檢查。也就是說,以下程式碼是合法的,並且只會在執行時出現錯誤(而不是靜態地!):

1
2
3
4
void main() {
dynamic x = 5;
x.aMethodThatDoesNotExist();
}

在 Dart 1 中,dynamic 無處不在,任何其他靜態類型都是為了 IDE 和靜態分析支援——但編譯器(和執行時)將所有東西都視為 dynamic。儘管如此,在 Dart 2 中仍然有一些不幸的「陷阱」可能會 意外地 建立一個動態類型的變數:

1
computeAge() => 5; // 返回類型是 dynamic
1
2
3
4
void main() {
var name; // 靜態類型是 dynamic
var animals = []; // 靜態和執行時類型是 List<dynamic>
}

更糟糕的是,dynamic 調用會丟失 Dart 2 中至關重要的類型資訊:

1
2
3
class User {
String name;
}
1
2
3
4
5
6
7
void main() {
var users = []; // 隱式地是 List<dynamic>,還記得嗎?
users.add(new User()..name = 'Matan');

// 執行時錯誤:List<dynamic> 不是 Iterable<String>
Iterable<String> names = users.map((u) => u.name);
}

發生此錯誤的原因是因為這裡的實際調用是:

1
users.map((dynamic u) => u.name);

…它沒有足夠的靜態類型資訊來產生 Iterable<String>。透過將 users 修正為正確的類型(並避免動態調用),一切正常:

1
2
3
4
5
6
7
void main() {
// 我們也可以寫成 `var users = <User>[
var users = [new User()..name = 'Matan'];

// OK!
Iterable<String> names = users.map((u) => u.name);
}

void

最後,我們有 void,這是 Dart 2 中最新的類型。在 Dart 1 中,void 只能用作函式的返回類型(例如 void main()),但在 Dart 2 中,它已被 泛化,並且可以在其他地方使用,例如 Future<void>

void 類型在語義上類似於 Object(它可以是任何東西),但有一些額外的限制——void 類型不能用於任何東西(即使是 ==hashCode),並且將某個東西賦值給 void 類型是無效的:

1
2
3
4
5
void foo() {}

void main() {
var bar = foo(); // 無效
}

實際應用中,我使用 void 來表示「任何東西,我不關心元素」,或者更常見的是表示「省略」,例如在 Future<void>Stream<void> 中:

1
2
/// 清除快取。
Future<void> purgeCache() { ... }

在上面的程式碼片段中,我不希望使用者嘗試使用提供的 Future 的返回值,因為它不相關。我見過使用 Future<Null> 來達到此目的的範例,這實際上是在 Future<void> 成為可能 之前 的一種解決方法。

例如,這在靜態上是正常的,但在執行時在 Dart 2 中是無效的:

1
2
3
4
5
6
7
8
9
import 'dart:async';

Future<String> _doAThing() async => 'Test';
Future<Null> doAThing() async => _doAThing();

void main() async {
// Future<String> 不是 FutureOr<Null> 類型的子類型
await doAThing();
}

…而使用 Future<void> 作為 doAThing() 是有效且正確的。

另一個例子可能是不帶任何事件資料的 Stream

1
2
/// 當使用者登出系統時觸發事件。
Stream<void> get onLogOut { ... }

另一個更實際的用途是實作一個具有您不會使用的泛型類型引數的類別。例如,實作流行的 訪問者模式,當 C(上下文)類型引數未使用時,我們可以透過傳遞 void 來忽略它:

1
2
3
4
5
6
7
8
abstract class Visitor<N, C> {
N visitNode(N node, [C context]);
}

class IdentityVisitor<N> extends Visitor<N, void> {
@override
N visitNode(N node, [_]) => node;
}

我希望這篇簡短的文章可以幫助您圍繞使用 Objectdynamicvoid 做出 API 決策。如果您有任何其他問題或想法,請留言!


Dart 2:void 的遺產 最初發佈於 Medium 上的 Dart,人們在那裡透過突出顯示和回應這個故事來繼續討論。