Effective Dart:用法
您可以在 Dart 程式碼主體中每天使用這些準則。您的函式庫的使用者可能無法判斷您是否已經內化了此處的想法,但其維護者肯定會知道。
函式庫
#這些準則可協助您以一致且可維護的方式,從多個檔案組成您的程式。為了使這些準則簡潔,它們使用「import」來涵蓋 import
和 export
指示詞。這些準則同樣適用於兩者。
在 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;
不要引入位於另一個套件 src
目錄內的函式庫
#Linter 規則:implementation_imports
lib
下的 src
目錄已指定包含套件本身實作的私人函式庫。套件維護者對其套件進行版本控制的方式會將此慣例納入考量。他們可以自由地大幅變更 src
下的程式碼,而不會對套件造成重大變更。
這表示,如果您引入其他套件的私人函式庫,則該套件的次要、理論上非重大的點發布版本可能會破壞您的程式碼。
不要讓匯入路徑進入或離開 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
目錄,並從套件中的其他位置引入函式庫。
優先使用相對匯入路徑
#Linter 規則:prefer_relative_imports
當先前的規則不適用時,請遵循此規則。當 import 未跨越 lib
目錄時,請優先使用相對 import。它們比較簡短。例如,假設您的目錄結構如下:
my_package
└─ lib
├─ src
│ └─ stuff.dart
│ └─ utils.dart
└─ api.dart
test
│─ api_test.dart
└─ test_utils.dart
以下是各個程式庫之間應如何互相 import:
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
#Linter 規則:avoid_init_to_null
如果變數具有不可為 null 的類型,如果您在變數被明確初始化之前嘗試使用它,Dart 會回報編譯錯誤。如果變數可為 null,則它會自動為您初始化為 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;
}
請勿使用明確的 null
預設值
#Linter 規則:avoid_init_to_null
如果您將可為 null 的參數設定為選用,但未提供預設值,則語言會隱式地使用 null
作為預設值,因此不需要寫入。
void error([String? message]) {
stderr.write(message ?? '\n');
}
void error([String? message = null]) {
stderr.write(message ?? '\n');
}
請勿在相等運算中使用 true
或 false
#使用相等運算子針對布林值字面值評估一個不可為 null 的布林值表達式是多餘的。總是可以使用刪除相等運算子,並在必要時使用一元否定運算子 !
來簡化。
if (nonNullableBool) { ... }
if (!nonNullableBool) { ... }
if (nonNullableBool == true) { ... }
if (nonNullableBool == false) { ... }
若要評估一個可為 null 的布林值表達式,您應該使用 ??
或明確的 != 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-aware 運算子 (例如 ??
) 不會將變數提升為不可為 null 的類型。如果您希望變數在 if
語句的主體內被提升,則最好使用明確的 != null
檢查,而不是 ??
。
如果您需要檢查 late
變數是否已初始化,請避免使用它
#Dart 沒有任何方法可以判斷 late
變數是否已初始化或賦值。如果您存取它,它會立即執行初始化程式 (如果有),或擲回例外狀況。有時候您會有延遲初始化的狀態,此時 late
可能很適合,但您也需要能夠判斷初始化是否已發生。
雖然您可以透過將狀態儲存在 late
變數中,並擁有一個單獨的布林值欄位來追蹤變數是否已設定來偵測初始化,但這是多餘的,因為 Dart 在內部維護 late
變數的初始化狀態。相反地,通常將變數設定為非 late
且可為 null 會更清楚。然後,您可以透過檢查 null
來查看變數是否已初始化。
當然,如果 null
是變數的有效初始化值,那麼擁有一個單獨的布林值欄位可能確實有意義。
考慮使用類型提升或空值檢查模式來使用可為空值的類型
#檢查可為 null 的變數是否不等於 null
會將變數提升為不可為 null 的類型。這可讓您存取變數上的成員,並將其傳遞給預期不可為 null 類型的函式。
但是,類型提升僅支援區域變數、參數和私有 final 欄位。容易被操作的值無法進行類型提升。
宣告成員私有和final(如同我們通常建議的那樣)通常足以繞過這些限制。但是,這並不總是可行的。
繞過類型提升限制的一種模式是使用null 檢查模式。這會在確認成員的值不是 null 的同時,將該值繫結至相同基本類型的新不可為 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 檢查會進行提升,因此您可以安全地將其視為不可為 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 檢查模式會比每次需要將值視為不可為 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 中撰寫字串時應記住的一些最佳做法。
使用相鄰字串來串連字串字面值
#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.');
優先使用插值來組合字串和值
#Linter 規則:prefer_interpolation_to_compose_strings
如果您來自其他語言,您會習慣使用一長串的 +
來根據字面值和其他值建立字串。這在 Dart 中確實可行,但使用內插法通常更乾淨、更簡短。
'Hello, $name! You are ${year - birth} years old.';
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
請注意,此準則適用於組合多個字面值和值。當僅將單個物件轉換為字串時,使用 .toString()
是可以的。
避免在不需要時在插值中使用大括號
#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 開箱即支援四種集合類型:列表、對應、佇列和集合。以下最佳做法適用於集合。
盡可能使用集合字面值
#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 類別也有未命名的建構函式,但在 null 安全 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')));
請勿使用 .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(' ');
請避免將 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()
始終是可以的。對應不可迭代,因此此準則不適用。
除非您打算變更結果的類型,否則請勿使用 List.from()
#給定一個 Iterable,有兩種明顯的方法可以產生一個包含相同元素的新列表
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()
。
請使用 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,且沒有不必要的包裝層級。
當附近的操作可以執行時,請勿使用 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) => 1 / n);
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();
請避免使用 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 中,即使是函式也是物件。以下是一些關於函式的最佳實務。
使用函數宣告將函數繫結至名稱
#程式碼檢查規則:prefer_function_declarations_over_variables
現代程式語言已經意識到本地巢狀函式和閉包有多麼有用。在一個函式內定義另一個函式是很常見的。在許多情況下,這個函式會立即用作回呼,而且不需要名稱。函式表達式非常適合用於這種情況。
但是,如果您確實需要為它命名,請使用函式宣告陳述式,而不是將 lambda 繫結到變數。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
當 tear-off 可以完成時,不要建立 lambda
#程式碼檢查規則:unnecessary_lambdas
當您在沒有括號的情況下參照函式、方法或具名建構函式時,Dart 會建立一個剝離。這是一個閉包,它會採用與函式相同的參數,並在您呼叫它時呼叫底層函式。如果您的程式碼需要一個閉包,該閉包會以與閉包接受的參數相同的參數呼叫具名函式,則不要將呼叫包裝在 lambda 中。請使用剝離。
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 中最佳地使用變數。
請遵循一致的規則,以在區域變數上使用 var
和 final
#大多數區域變數都不應具有類型註解,而應只使用 var
或 final
宣告。關於何時使用這兩種宣告,有兩種廣泛使用的規則
對於不重新指派的區域變數,請使用
final
,而對於重新指派的區域變數,請使用var
。對於所有區域變數,即使是不重新指派的變數,也請使用
var
。永遠不要對區域變數使用final
。(當然,仍然鼓勵對欄位和頂層變數使用final
。)
兩種規則皆可接受,但請選擇一種規則,並在整個程式碼中一致地應用它。這樣,當讀者看到 var
時,他們就知道這是否表示該變數會在函式中稍後指派。
避免儲存您可以計算的內容
#在設計類別時,您通常會想要公開同一底層狀態的多個檢視。您經常會看到在建構函式中計算所有這些檢視,然後將它們儲存起來的程式碼
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 中,物件具有成員,成員可以是函式(方法)或資料(執行個體變數)。以下最佳實務適用於物件的成員。
不要不必要地將欄位包裝在 getter 和 setter 中
#程式碼檢查規則:unnecessary_getters_setters
在 Java 和 C# 中,常見的做法是將所有欄位隱藏在 getter 和 setter(或 C# 中的屬性)之後,即使實作只是轉發到該欄位。這樣一來,如果您需要在這些成員中執行更多工作,您就可以執行,而無需接觸呼叫位置。這是因為呼叫 getter 方法與存取 Java 中的欄位不同,而且存取屬性與存取 C# 中的原始欄位不具二進位相容性。
Dart 沒有這種限制。欄位和 getter/setter 完全無法區分。您可以在類別中公開一個欄位,然後將其包裝在 getter 和 setter 中,而無需接觸任何使用該欄位的程式碼。
class Box {
Object? contents;
}
class Box {
Object? _contents;
Object? get contents => _contents;
set contents(Object? value) {
_contents = value;
}
}
請優先使用 final
欄位來建立唯讀屬性
#如果您有一個外部程式碼應可看見但不能指派給的欄位,在許多情況下,簡單的解決方案是直接將其標示為 final
。
class Box {
final contents = [];
}
class Box {
Object? _contents;
Object? get contents => _contents;
}
當然,如果您需要在建構函式外部在內部指派給該欄位,您可能需要執行「私有欄位、公有 getter」模式,但請在需要時才執行此操作。
請考慮對簡單的成員使用 =>
#程式碼檢查規則: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);
除非是為了重新導向至具名建構函式或避免遮蔽,否則請勿使用 this.
#程式碼檢查規則: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 初始化工具,這種程式碼相對罕見。
盡可能在宣告時初始化欄位
#如果欄位不依賴任何建構函式參數,則可以且應該在其宣告時初始化。當類別具有多個建構函式時,它需要較少的程式碼,並避免重複。
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
。
當然,如果欄位依賴建構函式參數,或由不同的建構函式以不同的方式初始化,則此準則不適用。
建構子
#以下最佳實務適用於宣告類別的建構函式。
盡可能使用初始化形式參數
#程式碼檢查規則: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.
語法稱為「初始化形式參數」。您無法總是加以利用。有時您會想要具有名稱與您正在初始化的欄位名稱不符的具名參數。但是,當您可以使用初始化形式參數時,您應該使用。
當建構函式初始化清單可以完成工作時,請勿使用 late
#Dart 要求您先初始化非可為 null 的欄位,然後才能讀取它們。由於可以在建構函式主體內讀取欄位,這表示如果您在主體執行之前未初始化非可為 null 的欄位,就會收到錯誤。
您可以將欄位標示為 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
並失去一些靜態安全性和效能來得好。
對於空的建構函式主體,請使用 ;
而不是 {}
#程式碼檢查規則: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) {}
}
請勿使用 new
#程式碼檢查規則: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!'),
],
);
}
請勿多餘地使用 const
#程式碼檢查規則:unnecessary_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 會使用例外狀況。以下最佳實務適用於捕獲和擲回例外狀況。
請避免使用沒有 on
子句的 catch
#程式碼檢查規則:avoid_catches_without_on_clauses
沒有 on
限定詞的 catch 子句會捕獲 try 區塊中程式碼擲回的任何項目。寶可夢例外狀況處理很可能不是您想要的。您的程式碼是否正確處理了 StackOverflowError 或 OutOfMemoryError?如果您在該 try 區塊中錯誤地將錯誤的引數傳遞給方法,您是希望偵錯工具將您指向錯誤,還是希望有用的 ArgumentError 被吞掉?您是否希望該程式碼內的任何 assert()
陳述式有效地消失,因為您正在捕獲擲回的 AssertionError?
答案可能為「否」,在這種情況下,您應該篩選您捕獲的類型。在大多數情況下,您應該使用 on
子句,將您限制為您知道且正在正確處理的執行階段失敗類型。
在少數情況下,您可能希望捕獲任何執行階段錯誤。這通常發生在框架或底層程式碼中,這些程式碼試圖隔離任意應用程式碼,避免其造成問題。即使在這種情況下,通常捕獲 Exception,而不是捕獲所有類型會更好。Exception 是所有執行階段錯誤的基底類別,並且排除表示程式碼中程式設計錯誤的錯誤。
不要丟棄沒有 on
子句的 catch 中的錯誤
#如果您真的覺得需要捕獲程式碼區域中可能拋出的所有內容,請對您捕獲的內容執行某些操作。記錄它、向使用者顯示它或重新拋出它,但不要靜默地丟棄它。
只對程式設計錯誤拋出實現 Error
的物件
#Error 類別是程式設計錯誤的基底類別。當拋出該類型或其子介面(例如 ArgumentError)的物件時,表示您的程式碼中存在錯誤。當您的 API 想要向呼叫者報告其使用不正確時,拋出 Error 會清楚地發出該訊號。
相反地,如果異常是某種執行階段失敗,不表示程式碼中的錯誤,那麼拋出 Error 會產生誤導。請改為拋出核心 Exception 類別之一或其他類型。
不要明確地捕獲 Error
或實現它的類型
#Linter 規則:avoid_catching_errors
這遵循上述規則。由於 Error 表示您的程式碼中存在錯誤,它應該展開整個呼叫堆疊、停止程式並列印堆疊追蹤,以便您可以找到並修復該錯誤。
捕獲這些類型的錯誤會破壞該流程並掩蓋錯誤。不要在事後新增錯誤處理程式碼來處理此異常,請返回並修復導致拋出它的程式碼。
使用 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 具有多個語言功能來支援非同步程式設計。以下最佳實務適用於非同步程式碼。
優先使用 async/await 而不是使用原始 futures
#非同步程式碼眾所周知難以閱讀和除錯,即使使用像 futures 這樣好的抽象概念也是如此。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;
} 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;
});
}).catchError((e) {
log.error(e);
return 0;
});
}
當 async
沒有任何作用時,不要使用它
#很容易養成在執行任何與非同步相關操作的任何函式上使用 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';
考慮使用高階方法轉換 stream
#這與上述關於可迭代物件的建議類似。串流支援許多相同的方法,並且還會正確處理諸如傳輸錯誤、關閉等問題。
避免直接使用 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 需要用於兩種底層程式碼:新的非同步基本類型,以及與不使用 futures 的非同步程式碼的介面。大多數其他程式碼應該使用 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>
的歧義時,請測試 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>
,它會錯誤地將其視為裸的同步值。
除非另有說明,否則本網站上的文件反映了 Dart 3.6.0。頁面最後更新時間為 2024-11-17。 檢視原始碼 或 回報問題。