Effective Dart:用法
您可以在日常 Dart 程式碼主體中使用這些指南。您的函式庫的使用者可能無法判斷您是否已將此處的想法內化,但函式庫的維護者肯定會知道。
函式庫
#這些指南可協助您以一致、可維護的方式,從多個檔案組成您的程式。為了保持這些指南簡潔,它們使用「import」來涵蓋 import
和 export
指令。這些指南同樣適用於兩者。
請務必在 part of
指令中使用字串
#Linter 規則:use_string_in_part_of_directives
許多 Dart 開發人員避免完全使用 part
。他們發現當每個函式庫都是單一檔案時,更容易理解他們的程式碼。如果您確實選擇使用 part
將函式庫的一部分分割到另一個檔案中,Dart 會要求另一個檔案反過來指示它是哪個函式庫的一部分。
Dart 允許 part of
指令使用函式庫的名稱。命名函式庫是一項舊版功能,現在已不建議使用。當判斷部件屬於哪個函式庫時,函式庫名稱可能會引入歧義。
慣用的語法是使用 URI 字串,直接指向函式庫檔案。如果您有一些函式庫 my_library.dart
,其中包含
library my_library;
part 'some/other/file.dart';
則部件檔案應使用函式庫檔案的 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
只要先前的規則沒有發揮作用,就請遵循此規則。當匯入未跨越 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
#請勿將變數明確初始化為 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 的機會時,才是正確的,但當它可能產生 null 時則不然。布林邏輯令人困惑。如果
nullableBool
為 null,則nullableBool == true
表示條件評估為false
。
??
運算子清楚地表明正在發生與 null 相關的事情,因此不會被誤認為是多餘的運算。邏輯也更清晰;運算式的結果為 null
與布林常值相同。
在條件內對變數使用空值感知運算子 (例如 ??
) 不會將變數提升為不可為 null 的類型。如果您希望變數在 if
陳述式的主體內提升,最好使用明確的 != null
檢查,而不是 ??
。
如果您需要檢查延遲變數是否已初始化,請避免使用 late
變數
#Dart 無法判斷 late
變數是否已初始化或已指派。如果您存取它,它會立即執行初始化程式 (如果有的話) 或擲回例外狀況。有時您有一些延遲初始化的狀態,其中 late
可能很適用,但您也需要能夠判斷初始化是否已發生。
雖然您可以透過將狀態儲存在 late
變數中,並使用追蹤變數是否已設定的個別布林欄位來偵測初始化,但這是多餘的,因為 Dart 在內部維護 late
變數的初始化狀態。相反地,通常更清楚的做法是將變數設為非 late
且可為 null。然後,您可以透過檢查 null
來查看變數是否已初始化。
當然,如果 null
是變數的有效初始化值,那麼擁有個別的布林欄位可能確實有意義。
考量使用類型提升或空值檢查模式來使用可為 null 的類型
#檢查可為 null 的變數是否不等於 null
會將變數提升為不可為 null 的類型。這可讓您存取變數上的成員,並將其傳遞至預期為不可為 null 類型的函式。
但是,類型提升僅支援區域變數、參數和私有 final 欄位。容易受到操作的值無法進行類型提升。
宣告成員為私有和final,正如我們通常建議的那樣,通常足以繞過這些限制。但是,這並不總是可行的選項。
繞過類型提升限制的一種模式是使用空值檢查模式。這會同時確認成員的值不是 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。
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 時都使用 !
更清晰且更安全
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 類別也有未命名的建構子,但在空值安全 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
。使用不需要您否定結果的那個。
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,有兩種明顯的方法可以產生包含相同元素的新 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()
。
請務必使用 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()
#通常,當您處理可迭代物件或 stream 時,您會對其執行多個轉換。最後,您想要產生具有特定類型引數的物件。與其附加對 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 中,即使函式也是物件。以下是一些涉及函式的最佳實務。
請務必使用函式宣告將函式繫結至名稱
#Linter 規則:prefer_function_declarations_over_variables
現代語言已經意識到區域巢狀函式和閉包有多有用。在另一個函式內定義函式是很常見的。在許多情況下,此函式會立即用作回呼,並且不需要名稱。函式運算式非常適合用於此目的。
但是,如果您確實需要給它一個名稱,請使用函式宣告陳述式,而不是將 lambda 繫結至變數。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
當可以使用 tear-off 時,請勿建立 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 中最佳地使用變數。
請務必在區域變數上遵循 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 中
#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;
}
}
偏好使用 final
欄位來建立唯讀屬性
#如果您有一個欄位,希望外部程式碼能夠看到但不能指派值,一個在許多情況下都適用的簡單解決方案是直接將其標記為 final
。
class Box {
final contents = [];
}
class Box {
Object? _contents;
Object? get contents => _contents;
}
當然,如果您需要在建構子之外的內部指派給欄位,您可能需要執行「私有欄位,公開 getter」模式,但在您需要之前不要使用它。
考慮對簡單成員使用 =>
#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);
除了重新導向到具名建構子或避免遮蔽之外,不要使用 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);
}
這看起來令人驚訝,但運作方式符合您的期望。幸運的是,由於初始化形式參數和父類別初始化器,像這樣的程式碼相對罕見。
請務必在可能時於宣告時初始化欄位
#如果欄位不依賴任何建構子參數,則可以在其宣告時初始化,並且應該這樣做。當類別有多個建構子時,它可以減少程式碼並避免重複。
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
。
當然,如果欄位依賴建構子參數,或由不同的建構子以不同的方式初始化,則此指南不適用。
建構子
#以下最佳實務適用於宣告類別的建構子。
請務必在可能時使用初始化形式參數
#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.
語法稱為「初始化形式參數」。您並非總是能利用它。有時您想要一個具名參數,其名稱與您正在初始化的欄位名稱不符。但是當您可以使用初始化形式參數時,您應該使用。
當建構子初始化列表可以完成時,不要使用 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
並失去一些靜態安全性和效能更好。
對於空的建構子主體,請使用 ;
而不是 {}
#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) {}
}
不要使用 new
#Linter 規則:unnecessary_new
呼叫建構子時,new
關鍵字是可選的。它的意義不明確,因為工廠建構子表示 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
#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 會使用例外。以下最佳實務適用於捕捉和拋出例外。
避免沒有 on
子句的 catch
#Linter 規則: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 具有多個語言功能來支援非同步程式設計。以下最佳實務適用於非同步編碼。
相較於使用原始 futures,偏好使用 async/await
#即使使用像 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;
} 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
#這與上面關於可迭代物件的建議類似。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 對於兩種低階程式碼是必要的:新的非同步原語,以及與不使用 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');
}
當消除 FutureOr<T>
的歧義時,測試 Future<T>
,其類型引數可能是 Object
#在您可以對 FutureOr<T>
執行任何有用的操作之前,您通常需要執行 is
檢查,以查看您是否擁有 Future<T>
或裸露的 T
。如果類型引數是像 FutureOr<int>
這樣的特定類型,則您使用哪個測試並不重要,is int
或 is Future<int>
。兩者都有效,因為這兩種類型是不相交的。
但是,如果值類型是 Object
或類型參數,該類型參數可能以 Object
實例化,則兩個分支重疊。Future<Object>
本身實作 Object
,因此即使物件是 future,is Object
或 is T
(其中 T
是可能以 Object
實例化的某些類型參數)也會傳回 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.7.1。頁面最後更新於 2025-02-12。 檢視原始碼 或 回報問題。