高效 Dart: 用法
你可以在日常的 Dart 程式碼中使用這些指南。你的庫的使用者可能看不出你已經內化了這裡的想法,但它的維護者肯定能看出來。
庫
#這些指南幫助你以一致、可維護的方式將程式由多個檔案組成。為了使這些指南簡明扼要,它們使用“import”來涵蓋 import 和 export 指令。這些指南對兩者同樣適用。
DO 在 part of 指令中使用字串
#Linter 規則: use_string_in_part_of_directives
許多 Dart 開發者完全避免使用 part。他們認為當每個庫都是一個檔案時,更容易理解程式碼。如果你選擇使用 part 將庫的一部分拆分到另一個檔案中,Dart 要求該檔案反過來指明它是哪個庫的一部分。
Dart 允許 part of 指令使用庫的名稱。為庫命名是一個遺留特性,現在已不推薦使用。在確定 part 屬於哪個庫時,庫名稱可能會引入歧義。
首選的語法是使用指向庫檔案的 URI 字串。如果你有一個庫 my_library.dart,其中包含
library my_library;
part 'some/other/file.dart';則 part 檔案應使用庫檔案的 URI 字串
part of '../../my_library.dart';而不是庫名稱
part of my_library;DON'T 匯入位於另一個包的 src 目錄中的庫
#Linter 規則: implementation_imports
lib 下的 src 目錄被指定包含包自身實現的私有庫。包維護者對包進行版本控制時會考慮此約定。他們可以自由地對 src 下的程式碼進行大幅更改,而無需將其視為對包的破壞性更改。
這意味著如果你匯入了其他包的私有庫,該包的一個次要的、理論上非破壞性的點版本更新可能會破壞你的程式碼。
DON'T 允許匯入路徑進入或離開 lib
#Linter 規則: avoid_relative_lib_imports
package: 匯入允許你訪問包的 lib 目錄內的庫,而無需擔心包儲存在計算機上的位置。為了使其正常工作,你不能有需要 lib 在磁碟上相對於其他檔案位於某個位置的匯入。換句話說,lib 目錄內檔案的相對匯入路徑不能超出 lib 目錄訪問外部檔案,而 lib 目錄外的庫也不能使用相對路徑進入 lib 目錄。這兩種做法都會導致令人困惑的錯誤和損壞的程式。
例如,假設你的目錄結構如下所示
my_package
└─ lib
└─ api.dart
test
└─ api_test.dart假設 api_test.dart 以兩種方式匯入 api.dart
import 'package:my_package/api.dart';
import '../lib/api.dart';Dart 認為這些是兩個完全不相關的庫的匯入。為了避免混淆 Dart 和你自己,請遵循以下兩條規則
- 不要在匯入路徑中使用
/lib/。 - 不要使用
../跳出lib目錄。
相反,當你需要進入包的 lib 目錄時(即使是從同一個包的 test 目錄或任何其他頂層目錄),請使用 package: 匯入。
import 'package:my_package/api.dart';包絕不應該跳出其 lib 目錄並從包中的其他地方匯入庫。
PREFER 使用相對匯入路徑
#Linter 規則: prefer_relative_imports
只要上一條規則不適用,就遵循本條規則。當匯入沒有跨越 lib 時,優先使用相對匯入。它們更短。例如,假設你的目錄結構如下所示
my_package
└─ lib
├─ src
│ └─ stuff.dart
│ └─ utils.dart
└─ api.dart
test
│─ api_test.dart
└─ test_utils.dart以下是各個庫應該如何相互匯入
import 'src/stuff.dart';
import 'src/utils.dart';import '../api.dart';
import 'stuff.dart';import 'package:my_package/api.dart'; // Don't reach into 'lib'.
import 'test_utils.dart'; // Relative within 'test' is fine.Null
#DON'T 顯式地將變數初始化為 null
#Linter 規則: avoid_init_to_null
如果變數是非可空型別,如果你在它明確初始化之前嘗試使用它,Dart 會報告編譯錯誤。如果變數是可空的,那麼它會為你隱式地初始化為 null。Dart 中沒有“未初始化記憶體”的概念,也不需要顯式地將變數初始化為 null 以確保“安全”。
Item? bestDeal(List<Item> cart) {
Item? bestItem;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}Item? bestDeal(List<Item> cart) {
Item? bestItem = null;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}DON'T 使用顯式的預設值 null
#Linter 規則: avoid_init_to_null
如果你將一個可空引數設為可選,但沒有給它預設值,語言會隱式地使用 null 作為預設值,因此無需寫出來。
void error([String? message]) {
stderr.write(message ?? '\n');
}void error([String? message = null]) {
stderr.write(message ?? '\n');
}DON'T 在相等性操作中使用 true 或 false
#對非可空布林表示式使用相等性運算子與布林字面量進行比較是多餘的。通常更簡單的方法是移除相等性運算子,並在必要時使用一元否定運算子 !
if (nonNullableBool) {
...
}
if (!nonNullableBool) {
...
}if (nonNullableBool == true) {
...
}
if (nonNullableBool == false) {
...
}要評估一個可空的布林表示式,你應該使用 ?? 或顯式的 != null 檢查。
// If you want null to result in false:
if (nullableBool ?? false) {
...
}
// If you want null to result in false
// and you want the variable to type promote:
if (nullableBool != null && nullableBool) {
...
}// Static error if null:
if (nullableBool) {
...
}
// If you want null to be false:
if (nullableBool == true) {
...
}nullableBool == true 是一個可行的表示式,但不應使用,原因如下
它沒有表明程式碼與
null有關。因為它與
null的關聯不明顯,所以很容易被誤認為是非可空的情況,此時相等性運算子是多餘的,可以刪除。但這僅在左側的布林表示式不可能產生 null 時才成立,而當它可能產生 null 時則不成立。布林邏輯令人困惑。如果
nullableBool為 null,那麼nullableBool == true意味著條件評估為false。
?? 運算子清楚地表明與 null 相關的事情正在發生,因此不會被誤認為是多餘的操作。邏輯也更清晰;表示式結果為 null 與布林字面量相同。
在條件內對變數使用諸如 ?? 的 null 感知運算子不會將變數提升為非可空型別。如果你希望在 if 語句體內部提升變數,最好使用顯式的 != null 檢查而不是 ??。
如果需要檢查 late 變數是否已初始化,則 AVOID 使用它們
#Dart 沒有辦法判斷一個 late 變數是否已經初始化或賦值。如果你訪問它,它會立即執行初始化程式(如果有的話)或丟擲異常。有時你可能有一些延遲初始化的狀態,late 可能很適合,但你也需要能夠判斷初始化是否已經發生。
儘管你可以透過將狀態儲存在 late 變數中並使用一個單獨的布林欄位來跟蹤變數是否已設定來檢測初始化,但這冗餘了,因為 Dart 內部維護著 late 變數的初始化狀態。相反,通常更清晰的方法是使變數非 late 且可空。然後你可以透過檢查 null 來判斷變數是否已初始化。
當然,如果 null 是變數的有效初始值,那麼有一個單獨的布林欄位可能確實有意義。
CONSIDER 對可空型別使用型別提升或 null-check 模式
#檢查可空變數不等於 null 會將變數提升為非可空型別。這允許你訪問該變數上的成員,並將其傳遞給期望非可空型別的函式。
然而,型別提升僅支援區域性變數、引數和私有 final 欄位。開放修改的值無法進行型別提升。
將成員宣告為私有和final,正如我們通常建議的,通常足以繞過這些限制。但是,這並非總是可行的選項。
繞過型別提升限制的一種模式是使用null-check pattern。這同時確認成員的值不為 null,並將該值繫結到具有相同基礎型別的新非可空變數。
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
if (this.response case var response?) {
return 'Could not complete upload to ${response.url} '
'(error code ${response.errorCode}): ${response.reason}.';
}
return 'Could not upload (no response).';
}
}另一種變通方法是將欄位的值賦給區域性變數。對該變數進行 null 檢查會觸發型別提升,因此你可以安全地將其視為非可空。
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
final response = this.response;
if (response != null) {
return 'Could not complete upload to ${response.url} '
'(error code ${response.errorCode}): ${response.reason}.';
}
return 'Could not upload (no response).';
}
}使用區域性變數時要小心。如果你需要寫回欄位,確保不要寫回區域性變數。(將區域性變數設為 final 可以防止此類錯誤。)此外,如果欄位在區域性變數作用域內可能發生變化,則區域性變數的值可能已過期。
有時最好直接對欄位使用 !。然而,在某些情況下,使用區域性變數或 null-check pattern 比每次需要將值視為非 null 時都使用 ! 更簡潔安全
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
if (response != null) {
return 'Could not complete upload to ${response!.url} '
'(error code ${response!.errorCode}): ${response!.reason}.';
}
return 'Could not upload (no response).';
}
}字串
#以下是在 Dart 中構建字串時需要記住的一些最佳實踐。
DO 使用相鄰字串來連線字串字面量
#Linter 規則: prefer_adjacent_string_concatenation
如果你有兩個字串字面量——不是值,而是實際的引用字面量形式——你不需要使用 + 來連線它們。就像 C 和 C++ 一樣,只需將它們並排放置即可。這是建立無法在一行中顯示的單個長字串的好方法。
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.',
);raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other ' +
'parts are overrun by martians. Unclear which are which.',
);PREFER 使用插值來構建字串和值
#Linter 規則: prefer_interpolation_to_compose_strings
如果你來自其他語言,你可能習慣於使用冗長的 + 鏈來從字面量和其他值構建字串。這在 Dart 中也能工作,但使用插值幾乎總是更清晰、更簡潔
'Hello, $name! You are ${year - birth} years old.';'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';請注意,本指南適用於組合多個字面量和值。當僅將單個物件轉換為字串時,使用 .toString() 是可以的。
AVOID 在不需要時在插值中使用花括號
#Linter 規則: unnecessary_brace_in_string_interps
如果你插值一個簡單識別符號,且後面沒有立即跟其他字母數字文字,則應省略 {}。
var greeting = 'Hi, $name! I love your ${decade}s costume.';var greeting = 'Hi, ${name}! I love your ${decade}s costume.';集合
#Dart 原生支援四種集合型別:列表、對映、佇列和集合。以下最佳實踐適用於集合。
DO 儘可能使用集合字面量
#Linter 規則: prefer_collection_literals
Dart 有三種核心集合型別:List、Map 和 Set。Map 和 Set 類像大多數類一樣具有無名建構函式。但由於這些集合使用非常頻繁,Dart 提供了更友好的內建語法來建立它們
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};var addresses = Map<String, Address>();
var counts = Set<int>();請注意,本指南不適用於這些類的命名建構函式。List.from()、Map.fromIterable() 等都有其用途。(List 類也有一個無名建構函式,但在空安全 Dart 中禁止使用。)
集合字面量在 Dart 中特別強大,因為它們提供了訪問擴充套件運算子的能力,用於包含其他集合的內容,以及在構建內容時執行控制流的if 和 for
var arguments = [
...options,
command,
...?modeFlags,
for (var path in filePaths)
if (path.endsWith('.dart')) path.replaceAll('.dart', '.js'),
];var arguments = <String>[];
arguments.addAll(options);
arguments.add(command);
if (modeFlags != null) arguments.addAll(modeFlags);
arguments.addAll(
filePaths
.where((path) => path.endsWith('.dart'))
.map((path) => path.replaceAll('.dart', '.js')),
);DON'T 使用 .length 來檢查集合是否為空
#Linter 規則: prefer_is_empty, prefer_is_not_empty
Iterable 契約不要求集合知道其長度或能夠在常數時間內提供長度。僅為了檢視集合是否包含任何內容而呼叫 .length 可能會非常慢。
相反,有更快、更易讀的 getter:.isEmpty 和 .isNotEmpty。使用那個不需要你對結果取反的 getter。
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');AVOID 對 Iterable.forEach() 使用函式字面量
#Linter 規則: avoid_function_literals_in_foreach_calls
forEach() 函式在 JavaScript 中廣泛使用,因為內建的 for-in 迴圈通常無法滿足你的需求。在 Dart 中,如果你想遍歷一個序列,慣用的方式是使用迴圈。
for (final person in people) {
...
}people.forEach((person) {
...
});請注意,本指南特別指出“函式字面量”。如果你想在每個元素上呼叫一些已存在的函式,使用 forEach() 是可以的。
people.forEach(print);另請注意,使用 Map.forEach() 總是可以的。Map 不可迭代,因此本指南不適用。
DON'T 使用 List.from(),除非你打算改變結果的型別
#給定一個 Iterable,有兩種顯而易見的方式來生成一個包含相同元素的新 List
var copy1 = iterable.toList();
var copy2 = List.from(iterable);顯而易見的區別是第一個更短。重要的區別是第一個保留了原始物件的型別引數
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<int>":
print(iterable.toList().runtimeType);// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);如果你想改變型別,那麼呼叫 List.from() 會很有用
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);但如果你的目標只是複製可迭代物件並保留其原始型別,或者你不在乎型別,那麼使用 toList()。
DO 使用 whereType() 根據型別過濾集合
#Linter 規則: prefer_iterable_whereType
假設你有一個包含混合物件的列表,並且只想從中獲取整數。你可以像這樣使用 where()
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);這很冗長,但更糟糕的是,它返回的可迭代物件型別可能不是你想要的。在此示例中,它返回一個 Iterable<Object>,儘管你可能想要一個 Iterable<int>,因為這是你正在過濾到的型別。
有時你會看到透過新增 cast() 來“修正”上述錯誤的程式碼
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();這很冗長,並且會建立兩個包裝器,帶來兩層間接定址和冗餘的執行時檢查。幸運的是,核心庫提供了whereType() 方法來解決此精確用例
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();使用 whereType() 簡潔明瞭,生成所需型別的 Iterable,並且沒有不必要的包裝層。
當附近的操作可以實現時,DON'T 使用 cast()
#通常當你處理可迭代物件或流時,會對其執行多次轉換。最後,你想要生成具有特定型別引數的物件。與其附加一個 cast() 呼叫,不如看看現有轉換中是否有可以改變型別的。
如果你已經呼叫了 toList(),將其替換為呼叫 List<T>.from(),其中 T 是你想要的列表結果型別。
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();如果你正在呼叫 map(),請為其提供一個顯式的型別引數,以便它生成所需型別的可迭代物件。型別推斷通常會根據你傳遞給 map() 的函式為你選擇正確的型別,但有時你需要明確指定。
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => n * 2);var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => n * 2).cast<double>();AVOID 使用 cast()
#這是上一條規則的更溫和的泛化。有時沒有附近的運算可以用來固定某個物件的型別。即使如此,如果可能,也要避免使用 cast() 來“改變”集合的型別。
優先使用以下任一選項
使用正確的型別建立它。更改首次建立集合的程式碼,使其具有正確的型別。
在訪問時轉換元素。如果你立即迭代集合,請在迭代內部轉換每個元素。
使用
List.from()急切地進行轉換。如果你最終會訪問集合中的大多數元素,並且不需要物件由原始活動物件支援,請使用List.from()進行轉換。cast()方法返回一個惰性集合,它在每次操作時檢查元素型別。如果你僅對少量元素執行少量操作,這種惰性可能不錯。但在許多情況下,惰性驗證和包裝的開銷超過了好處。
以下是使用正確型別建立它的示例:
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}以下是在訪問時轉換每個元素的示例:
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (final n in objects) {
if ((n as int).isEven) print(n);
}
}void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (final n in objects.cast<int>()) {
if (n.isEven) print(n);
}
}以下是使用 List.from() 急切地進行轉換的示例:
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = objects.cast<int>();
ints.sort();
return ints[ints.length ~/ 2];
}這些替代方法當然並非總是有效,有時 cast() 也是正確答案。但請記住,該方法存在一定風險且不受歡迎——它可能很慢,並且如果你不小心,可能會在執行時失敗。
函式
#在 Dart 中,即使是函式也是物件。以下是一些涉及函式的最佳實踐。
DO 使用函式宣告將函式繫結到名稱
#Linter 規則: prefer_function_declarations_over_variables
現代語言已經意識到區域性巢狀函式和閉包的實用性。在一個函式內部定義另一個函式是很常見的。在許多情況下,這個函式立即用作回撥,並且不需要名稱。函式表示式對此非常有用。
但是,如果你確實需要給它一個名稱,請使用函式宣告語句,而不是將 lambda 繫結到變數。
void main() {
void localFunction() {
...
}
}void main() {
var localFunction = () {
...
};
}當 tear-off 可以實現時,DON'T 建立 lambda
#Linter 規則: unnecessary_lambdas
當你在沒有括號的情況下引用函式、方法或命名建構函式時,Dart 會建立一個tear-off。這是一個閉包,它接受與該函式相同的引數,並在你呼叫它時呼叫底層函式。如果你的程式碼需要一個閉包來呼叫接受與閉包相同引數的命名函式,不要將呼叫包裝在 lambda 中。使用 tear-off。
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach(print);
// Method:
charCodes.forEach(buffer.write);
// Named constructor:
var strings = charCodes.map(String.fromCharCode);
// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach((code) {
print(code);
});
// Method:
charCodes.forEach((code) {
buffer.write(code);
});
// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));
// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));變數
#以下最佳實踐描述瞭如何在 Dart 中最佳使用變數。
DO 在區域性變數上遵循一致的 var 和 final 規則
#大多數區域性變數不應具有型別註解,應僅使用 var 或 final 宣告。關於何時使用其中之一,有兩種廣泛使用的規則
對於未重新賦值的區域性變數使用
final,對於重新賦值的區域性變數使用var。對所有區域性變數使用
var,即使是未重新賦值的變數。永遠不要對區域性變數使用final。(當然,仍然鼓勵對欄位和頂級變數使用final。)
任何一條規則都是可以接受的,但請選擇一條並在你的程式碼中始終如一地應用它。這樣,當讀者看到 var 時,他們就知道這是否意味著該變數稍後在函式中會被賦值。
AVOID 儲存可以計算出的值
#在設計類時,你通常希望公開對同一底層狀態的多種檢視。你經常看到程式碼在建構函式中計算所有這些檢視然後儲存它們
class Circle {
double radius;
double area;
double circumference;
Circle(double radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}這段程式碼有兩個問題。首先,它可能浪費記憶體。嚴格來說,面積和周長是快取。它們是儲存的計算結果,我們可以從已有的其他資料重新計算。它們正在用增加的記憶體換取減少的 CPU 使用。我們確定存在需要這種權衡的效能問題嗎?
更糟糕的是,程式碼是錯誤的。快取的問題在於失效——你怎麼知道快取何時過期需要重新計算?在這裡,我們永遠不知道,即使 radius 是可變的。你可以賦一個不同的值,area 和 circumference 將保留其之前的、現在不正確的值。
要正確處理快取失效,我們需要這樣做
class Circle {
double _radius;
double get radius => _radius;
set radius(double value) {
_radius = value;
_recalculate();
}
double _area = 0.0;
double get area => _area;
double _circumference = 0.0;
double get circumference => _circumference;
Circle(this._radius) {
_recalculate();
}
void _recalculate() {
_area = pi * _radius * _radius;
_circumference = pi * 2.0 * _radius;
}
}要編寫、維護、除錯和閱讀這些程式碼非常麻煩。相反,你的第一個實現應該是
class Circle {
double radius;
Circle(this.radius);
double get area => pi * radius * radius;
double get circumference => pi * 2.0 * radius;
}這段程式碼更短,使用更少的記憶體,並且更不容易出錯。它儲存表示圓所需的最小資料量。由於只有一個單一的真相來源,因此不會出現欄位不同步的情況。
在某些情況下,你可能需要快取慢速計算的結果,但請僅在你確定存在效能問題後才這樣做,並且要小心處理,並留下注釋解釋該最佳化。
成員
#在 Dart 中,物件擁有成員,它們可以是函式(方法)或資料(例項變數)。以下最佳實踐適用於物件的成員。
DON'T 不必要地將欄位包裝在 getter 和 setter 中
#Linter 規則: unnecessary_getters_setters
在 Java 和 C# 中,通常會將所有欄位隱藏在 getter 和 setter(或 C# 中的屬性)之後,即使實現只是轉發到欄位。這樣一來,如果你需要在這些成員中執行更多工作,你可以不必修改呼叫點。這是因為在 Java 中呼叫 getter 方法與訪問欄位不同,而在 C# 中訪問屬性與訪問原始欄位不是二進位制相容的。
Dart 沒有這個限制。欄位和 getter/setter 完全無法區分。你可以在類中公開一個欄位,然後將其包裝在 getter 和 setter 中,而無需修改任何使用該欄位的程式碼。
class Box {
Object? contents;
}class Box {
Object? _contents;
Object? get contents => _contents;
set contents(Object? value) {
_contents = value;
}
}PREFER 使用 final 欄位來建立只讀屬性
#如果你有一個欄位,外部程式碼應該能夠看到但不能賦值,一個在許多情況下都奏效的簡單解決方案是將其標記為 final。
class Box {
final contents = [];
}class Box {
Object? _contents;
Object? get contents => _contents;
}當然,如果你需要在建構函式外部內部為欄位賦值,你可能需要使用“私有欄位,公共 getter”模式,但在需要之前不要這樣做。
CONSIDER 對簡單成員使用 =>
#Linter 規則: prefer_expression_function_bodies
除了對函式表示式使用 => 外,Dart 還允許你使用它定義成員。這種風格非常適合僅計算並返回值的簡單成員。
double get area => (right - left) * (bottom - top);
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';編寫程式碼的人似乎很喜歡 =>,但濫用它很容易導致程式碼難以閱讀。如果你的宣告超過幾行或包含深度巢狀的表示式——級聯和條件運算子是常見的罪魁禍首——為了你自己以及所有需要閱讀你程式碼的人好,請使用塊體和一些語句。
Treasure? openChest(Chest chest, Point where) {
if (_opened.containsKey(chest)) return null;
var treasure = Treasure(where);
treasure.addAll(chest.contents);
_opened[chest] = treasure;
return treasure;
}Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
? null
: _opened[chest] = (Treasure(where)..addAll(chest.contents));你也可以對不返回值的成員使用 =>。當 setter 很小且有對應的使用 => 的 getter 時,這是慣用法。
num get x => center.x;
set x(num value) => center = Point(value, center.y);DON'T 使用 this.,除非是為了重定向到命名建構函式或避免遮蓋
#Linter 規則: unnecessary_this
JavaScript 需要顯式的 this. 來引用當前正在執行方法的物件上的成員,但 Dart——就像 C++、Java 和 C# 一樣——沒有這個限制。
只有兩種情況下你需要使用 this.。一種是當同名的區域性變數遮蓋了你想要訪問的成員時
class Box {
Object? value;
void clear() {
this.update(null);
}
void update(Object? value) {
this.value = value;
}
}class Box {
Object? value;
void clear() {
update(null);
}
void update(Object? value) {
this.value = value;
}
}另一種使用 this. 的情況是重定向到命名建構函式時
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// This won't parse or compile!
// ShadeOfGray.alsoBlack() : black();
}class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// But now it will!
ShadeOfGray.alsoBlack() : this.black();
}請注意,建構函式引數永遠不會在建構函式初始化列表中遮蓋欄位
class Box extends BaseBox {
Object? value;
Box(Object? value) : value = value, super(value);
}這看起來令人驚訝,但會按你期望的方式工作。幸運的是,由於初始化形參和 super 初始化器,此類程式碼相對較少見。
DO 儘可能在宣告時初始化欄位
#如果一個欄位不依賴於任何建構函式引數,那麼它可以在宣告時初始化,也應該在宣告時初始化。當類有多個建構函式時,這樣可以減少程式碼量並避免重複。
class ProfileMark {
final String name;
final DateTime start;
ProfileMark(this.name) : start = DateTime.now();
ProfileMark.unnamed() : name = '', start = DateTime.now();
}class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}有些欄位無法在宣告時初始化,因為它們需要引用 this——例如,使用其他欄位或呼叫方法。然而,如果欄位被標記為 late,那麼初始化程式就可以訪問 this。
當然,如果欄位依賴於建構函式引數,或者由不同的建構函式以不同的方式初始化,則本指南不適用。
建構函式
#以下最佳實踐適用於為類宣告建構函式。
DO 儘可能使用初始化形參 (initializing formals)
#Linter 規則: prefer_initializing_formals
許多欄位直接由建構函式引數初始化,例如
class Point {
double x, y;
Point(double x, double y) : x = x, y = y;
}這裡我們需要輸入 x 四次來定義一個欄位。我們可以做得更好
class Point {
double x, y;
Point(this.x, this.y);
}建構函式引數前的 this. 語法稱為“初始化形參 (initializing formal)”。你並非總是能利用它。有時你希望有一個命名引數,其名稱與你正在初始化的欄位名稱不匹配。但是,當你可以使用初始化形參時,你應該使用。
當建構函式初始化列表可以實現時,DON'T 使用 late
#Dart 要求你在讀取非可空欄位之前初始化它們。由於欄位可以在建構函式體內部讀取,這意味著如果你在主體執行之前未初始化非可空欄位,你會收到錯誤。
你可以透過將欄位標記為 late 來消除此錯誤。如果你在欄位初始化之前訪問它,這會將編譯時錯誤轉換為執行時錯誤。在某些情況下你需要這樣做,但通常正確的解決方法是在建構函式初始化列表中初始化欄位
class Point {
double x, y;
Point.polar(double theta, double radius)
: x = cos(theta) * radius,
y = sin(theta) * radius;
}class Point {
late double x, y;
Point.polar(double theta, double radius) {
x = cos(theta) * radius;
y = sin(theta) * radius;
}
}初始化列表讓你能夠訪問建構函式引數,並在欄位可讀之前對其進行初始化。因此,如果可以使用初始化列表,那比將欄位設為 late 並損失一些靜態安全性和效能要好。
DO 對空建構函式體使用 ; 而不是 {}
#Linter 規則: empty_constructor_bodies
在 Dart 中,具有空主體的建構函式可以使用分號結束。(實際上,對於 const 建構函式,這是必需的。)
class Point {
double x, y;
Point(this.x, this.y);
}class Point {
double x, y;
Point(this.x, this.y) {}
}DON'T 使用 new
#Linter 規則: unnecessary_new
呼叫建構函式時,new 關鍵字是可選的。它的含義不明確,因為 factory 建構函式意味著 new 呼叫實際上可能不返回新物件。
語言仍然允許使用 new,但將其視為已棄用,並避免在你的程式碼中使用它。
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(child: Text('Increment')),
Text('Click!'),
],
);
}Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(child: new Text('Increment')),
new Text('Click!'),
],
);
}DON'T 重複使用 const
#Linter 規則: unnecessary_const
在表示式必須是常量的上下文中,const 關鍵字是隱式的,不需要寫,也不應該寫。這些上下文是任何表示式內部
- const 集合字面量。
- const 建構函式呼叫
- 元資料註解。
- const 變數宣告的初始化程式。
- switch case 表示式——緊跟在
case之後但在:之前的部分,而不是 case 的主體。
(預設值不包含在此列表中,因為 Dart 的未來版本可能支援非 const 預設值。)
基本上,任何寫 new 而不是 const 會出錯的地方,Dart 都允許你省略 const。
const primaryColors = [
Color('red', [255, 0, 0]),
Color('green', [0, 255, 0]),
Color('blue', [0, 0, 255]),
];const primaryColors = const [
const Color('red', const [255, 0, 0]),
const Color('green', const [0, 255, 0]),
const Color('blue', const [0, 0, 255]),
];錯誤處理
#當程式中發生錯誤時,Dart 使用異常。以下最佳實踐適用於捕獲和丟擲異常。
AVOID 沒有 on 子句的 catch
#Linter 規則: avoid_catches_without_on_clauses
沒有 on 限定符的 catch 子句會捕獲 try 塊中的程式碼丟擲的任何內容。Pokémon 異常處理很可能不是你想要的。你的程式碼是否正確處理 StackOverflowError 或 OutOfMemoryError?如果你在該 try 塊中錯誤地向方法傳遞了錯誤的引數,你是希望偵錯程式指向錯誤,還是希望有用的 ArgumentError 被吞掉?你希望該程式碼中的任何 assert() 語句因為你捕獲了丟擲的 AssertionError 而實際上消失嗎?
答案很可能是“否”,在這種情況下,你應該過濾你捕獲的型別。在大多數情況下,你應該有一個 on 子句,將你限制在你瞭解並正在正確處理的執行時故障類型範圍內。
在極少數情況下,你可能希望捕獲任何執行時錯誤。這通常出現在嘗試隔離任意應用程式程式碼以防止其引起問題的框架或底層程式碼中。即使在這種情況下,捕獲 Exception 通常也比捕獲所有型別要好。Exception 是所有執行時錯誤的基類,不包括指示程式碼中存在程式性錯誤的錯誤。
DON'T 丟棄沒有 on 子句的 catch 中的錯誤
#如果你確實覺得需要捕獲程式碼塊中可以丟擲的所有內容,請對捕獲到的內容進行處理。記錄它,顯示給使用者,或者重新丟擲它,但不要靜默丟棄它。
DO 僅對程式錯誤丟擲實現 Error 的物件
#Error 類是程式性錯誤的基類。當丟擲該型別或其子介面(如 ArgumentError)的物件時,這意味著你的程式碼中存在錯誤。當你的 API 想要向呼叫者報告它被錯誤使用時,丟擲 Error 會清晰地傳送該訊號。
相反,如果異常是某種執行時故障,不表示程式碼中存在錯誤,那麼丟擲 Error 會產生誤導。此時,應丟擲核心 Exception 類之一或其他型別。
DON'T 顯式捕獲 Error 或實現它的型別
#Linter 規則: avoid_catching_errors
這是前述內容的推論。由於 Error 表示你的程式碼中存在錯誤,它應該展開整個呼叫堆疊,中止程式,並列印堆疊跟蹤,以便你可以定位並修復錯誤。
捕獲這些型別的錯誤會中斷該過程並掩蓋錯誤。與其事後新增錯誤處理程式碼來處理此異常,不如回到程式碼中修復導致它被丟擲的原因。
DO 使用 rethrow 重新丟擲捕獲的異常
#Linter 規則: use_rethrow_when_possible
如果你決定重新丟擲異常,優先使用 rethrow 語句而不是使用 throw 丟擲同一個異常物件。rethrow 保留了異常的原始堆疊跟蹤。而 throw 則會將堆疊跟蹤重置到最後丟擲的位置。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}非同步
#Dart 有多種語言特性來支援非同步程式設計。以下最佳實踐適用於非同步編碼。
PREFER 使用 async/await 而不是原始 future
#非同步程式碼以難以閱讀和除錯而聞名,即使使用了 Future 這樣的良好抽象。async/await 語法提高了可讀性,並允許你在非同步程式碼中使用 Dart 的所有控制流結構。
Future<int> countActivePlayers(String teamName) async {
try {
var team = await downloadTeam(teamName);
if (team == null) return 0;
var players = await team.roster;
return players.where((player) => player.isActive).length;
} on DownloadException catch (e, _) {
log.error(e);
return 0;
}
}Future<int> countActivePlayers(String teamName) {
return downloadTeam(teamName)
.then((team) {
if (team == null) return Future.value(0);
return team.roster.then((players) {
return players.where((player) => player.isActive).length;
});
})
.onError<DownloadException>((e, _) {
log.error(e);
return 0;
});
}當 async 沒有有用效果時,DON'T 使用它
#很容易養成在任何與非同步相關的函式上都使用 async 的習慣。但在某些情況下,它是多餘的。如果你可以在不改變函式行為的情況下省略 async,那就這麼做。
Future<int> fastestBranch(Future<int> left, Future<int> right) {
return Future.any([left, right]);
}Future<int> fastestBranch(Future<int> left, Future<int> right) async {
return Future.any([left, right]);
}async 有用的情況包括
你正在使用
await。(這是顯而易見的一點。)你正在非同步返回一個錯誤。
async然後throw比return Future.error(...)更短。你正在返回一個值,並且希望它隱式地被包裝在 future 中。
async比Future.value(...)更短。
Future<void> usesAwait(Future<String> later) async {
print(await later);
}
Future<void> asyncError() async {
throw 'Error!';
}
Future<String> asyncValue() async => 'value';CONSIDER 使用高階方法來轉換流
#這與上面關於 iterables 的建議相似。Stream 支援許多相同的方法,並且也能正確處理諸如傳輸錯誤、關閉等事情。
AVOID 直接使用 Completer
#許多剛接觸非同步程式設計的人想要編寫生成 Future 的程式碼。Future 中的建構函式似乎不符合他們的需求,所以他們最終找到了 Completer 類並使用了它。
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}Completer 需要用於兩種低層程式碼:新的非同步原語,以及與不使用 Future 的非同步程式碼進行介面。大多數其他程式碼應該使用 async/await 或 Future.then(),因為它們更清晰,並且使錯誤處理更容易。
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}Future<bool> fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains('bear');
}在區分型別引數可能為 Object 的 FutureOr<T> 時,DO 檢查 Future<T>
#在你可以對 FutureOr<T> 做任何有用的事情之前,你通常需要進行一次 is 檢查,以檢視你擁有的是一個 Future<T> 還是一個裸露的 T。如果型別引數是某個特定型別,如 FutureOr<int>,你使用哪種檢查(is int 還是 is Future<int>)都無關緊要。兩者都有效,因為這兩種型別是互斥的。
然而,如果值型別是 Object 或可能用 Object 例項化的型別引數,那麼這兩個分支就會重疊。Future<Object> 本身實現了 Object,因此 is Object 或 is T(其中 T 是可能用 Object 例項化的型別引數)即使當物件是 future 時也會返回 true。因此,應顯式檢查 Future 情況
Future<T> logValue<T>(FutureOr<T> value) async {
if (value is Future<T>) {
var result = await value;
print(result);
return result;
} else {
print(value);
return value;
}
}Future<T> logValue<T>(FutureOr<T> value) async {
if (value is T) {
print(value);
return value;
} else {
var result = await value;
print(result);
return result;
}
}在錯誤示例中,如果你傳遞給它一個 Future<Object>,它會錯誤地將其視為裸露的同步值。