內容

Effective Dart:用法

內容 keyboard_arrow_down keyboard_arrow_up
more_horiz

您可以在 Dart 程式碼的主體中每天使用這些準則。使用者可能無法看出您已內化此處的想法,但維護人員肯定會發現。

函式庫

#

這些準則可協助您以一致且可維護的方式,從多個檔案撰寫程式。為了簡潔起見,這些準則使用「import」涵蓋 importexport 指令。這些準則同樣適用於兩者。

請在 part of 指令中使用字串

#

Linter 規則:use_string_in_part_of_directives

許多 Dart 開發人員完全避免使用 part。當每個函式庫都是單一檔案時,他們發現更容易理解其程式碼。如果您選擇使用 part 將函式庫的一部分拆分到另一個檔案,Dart 會要求另一個檔案反過來指出它是哪個函式庫的一部分。

Dart 允許 part of 指令使用函式庫的名稱。命名函式庫是現已不建議使用的舊功能。在判斷 part 屬於哪個函式庫時,函式庫名稱可能會造成歧義。

建議的語法是使用直接指向函式庫檔案的 URI 字串。如果您有一些函式庫 my_library.dart,其中包含

my_library.dart
dart
library my_library;

part 'some/other/file.dart';

然後 part 檔案應使用函式庫檔案的 URI 字串

gooddart
part of '../../my_library.dart';

而不是函式庫名稱

baddart
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

api_test.dart
baddart
import 'package:my_package/api.dart';
import '../lib/api.dart';

Dart 將這些視為兩個完全無關的函式庫的匯入。為避免讓 Dart 和您感到困惑,請遵循以下兩個規則

  • 不要在匯入路徑中使用 /lib/
  • 不要使用 ../ 來跳脫 lib 目錄。

相反地,當您需要進入套件的 lib 目錄時(即使來自同一個套件的 test 目錄或任何其他頂層目錄),請使用 package: 匯入。

api_test.dart
gooddart
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

以下是各個函式庫應如何互相匯入

lib/api.dart
gooddart
import 'src/stuff.dart';
import 'src/utils.dart';
lib/src/utils.dart
gooddart
import '../api.dart';
import 'stuff.dart';
test/api_test.dart
gooddart
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 以確保「安全」。

gooddart
Item? bestDeal(List<Item> cart) {
  Item? bestItem;

  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }

  return bestItem;
}
baddart
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 作為預設值,因此無需撰寫。

gooddart
void error([String? message]) {
  stderr.write(message ?? '\n');
}
baddart
void error([String? message = null]) {
  stderr.write(message ?? '\n');
}

不要在等式運算中使用 truefalse

#

使用等式運算子來評估非 Null 布林表達式與布林文字之間的關係是多餘的。消除等式運算子並在必要時使用一元否定運算子 ! 始終較為簡單

gooddart
if (nonNullableBool) { ... }

if (!nonNullableBool) { ... }
baddart
if (nonNullableBool == true) { ... }

if (nonNullableBool == false) { ... }

要評估為 Null 的布林表達式,您應使用 ?? 或明確的 != null 檢查。

gooddart
// 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) { ... }
baddart
// 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 感知運算子(例如 ??)並不會將變數提升為非 null 類型。如果你想要在 if 陳述式的本體中提升變數,最好使用明確的 != null 檢查,而不是 ??

如果你需要檢查變數是否已初始化,請避免使用 late 變數

#

Dart 沒有辦法判斷 late 變數是否已初始化或已指派。如果你存取它,它會立即執行初始化程式(如果有的話),或擲回例外。有時你有一些延遲初始化的狀態,late 可能很適合,但你也需要能夠判斷初始化是否已發生。

雖然你可以透過將狀態儲存在 late 變數中並有一個單獨的布林欄位來追蹤變數是否已設定來偵測初始化,但這是多餘的,因為 Dart 內部會維護 late 變數的初始化狀態。相反地,通常更清楚的做法是讓變數為非 late 且可為 null。然後,你可以透過檢查 null 來查看變數是否已初始化。

當然,如果 null 是變數有效的初始化值,那麼有單獨的布林欄位可能是合理的。

考慮類型提升或 null 檢查模式,以使用可為 null 的類型

#

檢查可為 null 的變數是否不等於 null 會將變數提升為非 null 類型。這讓你可以在變數上存取成員,並將它傳遞給需要非 null 類型的函式。

不過,類型提升僅支援局部變數、參數和私有 final 欄位。開放給操作的值無法提升類型

宣告成員私有final(這是我們通常建議的做法),通常足以繞過這些限制。但是,這並不總是可行的選項。

一種用來解決類型提升限制的模式是使用null 檢查模式。這會同時確認成員的值不為 null,並將該值繫結到同一個基本類型的新的非 null 變數。

gooddart
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。

gooddart
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 時都使用 ! 更簡潔、更安全。

baddart
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++ 中一樣,只需將它們並排放置即可。這是製作單一長字串(不適合一行)的好方法。

gooddart
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');
baddart
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 中確實可行,但使用內插幾乎總是更簡潔、更簡短。

gooddart
'Hello, $name! You are ${year - birth} years old.';
baddart
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

請注意,此準則適用於組合多個文字和值。僅將單一物件轉換為字串時,可以使用 .toString()

避免在內插中使用大括號(若非必要)

#

Linter 規則:unnecessary_brace_in_string_interps

如果你內插一個簡單的識別碼,其後沒有緊接著更多字母數字文字,則應省略 {}

gooddart
var greeting = 'Hi, $name! I love your ${decade}s costume.';
baddart
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';

集合

#

開箱即用,Dart 支援四種集合類型:清單、映射、佇列和集合。下列最佳實務適用於集合。

請在可能的情況下使用集合文字

#

Linter 規則:prefer_collection_literals

Dart 有三個核心集合類型:清單、映射和集合。映射和集合類別有未命名建構函數,就像大多數類別一樣。但由於這些集合使用得非常頻繁,因此 Dart 有更友善的內建語法來建立它們

gooddart
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
baddart
var addresses = Map<String, Address>();
var counts = Set<int>();

請注意,此準則不適用於這些類別的命名建構函數。List.from()Map.fromIterable() 和朋友們都有其用途。(清單類別也有未命名建構函數,但它在 null 安全的 Dart 中被禁止。)

集合文字在 Dart 中特別強大,因為它們讓您可以存取散布運算子,以包含其他集合的內容,以及iffor,以便在建立內容時執行控制流程

gooddart
var arguments = [
  ...options,
  command,
  ...?modeFlags,
  for (var path in filePaths)
    if (path.endsWith('.dart')) path.replaceAll('.dart', '.js')
];
baddart
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_emptyprefer_is_not_empty

Iterable 合約不要求集合知道其長度或能夠在恆定時間內提供它。呼叫 .length 只是為了查看集合是否包含任何東西,這可能會非常慢。

相反地,有更快且更易讀的 getter:.isEmpty.isNotEmpty。使用不需要您否定結果的那個。

gooddart
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
baddart
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 中,如果您想對序列進行反覆運算,慣用的方法是使用迴圈。

gooddart
for (final person in people) {
  ...
}
baddart
people.forEach((person) {
  ...
});

請注意,此準則特別說明「函式文字」。如果您想對每個元素呼叫一些已經存在的函式,forEach() 是可以的。

gooddart
people.forEach(print);

另請注意,使用 Map.forEach() 始終是可以的。映射不可反覆運算,因此此準則不適用。

不要使用 List.from(),除非您打算變更結果的類型

#

給定一個 Iterable,有兩種明顯的方法可以產生一個包含相同元素的新清單

dart
var copy1 = iterable.toList();
var copy2 = List.from(iterable);

明顯的差異是第一個較短。重要的差異是第一個會保留原始物件的類型引數

gooddart
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<int>":
print(iterable.toList().runtimeType);
baddart
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);

如果您想要變更類型,那麼呼叫 List.from() 會很有用

gooddart
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

但如果您的目標只是複製 Iterable 並保留其原始類型,或者您不關心類型,請使用 toList()

使用 whereType() 來根據類型篩選集合

#

Linter 規則:prefer_iterable_whereType

假設您有一個包含混合物件的清單,並且您只想從中取得整數。您可以像這樣使用 where()

baddart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);

這很冗長,但更糟的是,它會傳回一個可迭代物件,其類型可能不是你想要的。在這裡的範例中,它會傳回一個 Iterable<Object>,儘管你可能想要一個 Iterable<int>,因為那是你過濾它的類型。

有時你會看到透過加入 cast() 來「修正」上述錯誤的程式碼

baddart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();

這很冗長,而且會導致建立兩個包裝器,包含兩層間接層級和多餘的執行時期檢查。幸運的是,核心函式庫有 whereType() 方法,可針對這個確切的使用案例

gooddart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();

使用 whereType() 很簡潔,會產生一個具有所需類型的 Iterable,而且沒有不必要的包裝層級。

當附近有其他作業可以執行時,不要使用 cast()

#

在處理可迭代物件或串流時,你通常會對其執行多個轉換。最後,你會想要產生一個具有特定類型引數的物件。不要加上呼叫 cast(),請查看現有的轉換之一是否可以變更類型。

如果你已經呼叫 toList(),請將其替換為呼叫 List<T>.from(),其中 T 是你想要的結果清單類型。

gooddart
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
baddart
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();

如果你正在呼叫 map(),請給它一個明確的類型引數,以便它產生一個具有所需類型的可迭代物件。類型推論通常會根據你傳遞給 map() 的函式為你選擇正確的類型,但有時你需要明確說明。

gooddart
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
baddart
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();

避免使用 cast()

#

這是前一條規則較為寬鬆的概括。有時沒有可以使用的附近作業來修正某些物件的類型。即使如此,在可能的情況下,避免使用 cast() 來「變更」集合的類型。

優先選擇以下任何選項

  • 使用正確的類型建立它。變更最初建立集合的程式碼,使其具有正確的類型。

  • 在存取時轉換元素。如果你立即迭代集合,請在迭代中轉換每個元素。

  • 使用 List.from() 積極轉換。如果你最終會存取集合中的大多數元素,而且不需要物件由原始動態物件支援,請使用 List.from() 轉換它。

    cast() 方法會傳回一個惰性集合,它會在每個作業中檢查元素類型。如果你只對少數元素執行少數作業,這種惰性會很好。但在許多情況下,惰性驗證和包裝的開銷會超過好處。

以下是使用正確的類型建立它的範例:

gooddart
List<int> singletonList(int value) {
  var list = <int>[];
  list.add(value);
  return list;
}
baddart
List<int> singletonList(int value) {
  var list = []; // List<dynamic>.
  list.add(value);
  return list.cast<int>();
}

以下是在存取時轉換每個元素

gooddart
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);
  }
}
baddart
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() 進行急切轉型的範例:

gooddart
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];
}
baddart
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 繫結到變數。

gooddart
void main() {
  void localFunction() {
    ...
  }
}
baddart
void main() {
  var localFunction = () {
    ...
  };
}

請勿建立 lambda,若可使用 tear-off

#

Linter 規則:unnecessary_lambdas

當您參照函式、方法或命名建構函式,但省略括號時,Dart 會建立一個截斷,也就是一個封閉,它會採用與函式相同的參數,並在您呼叫它時呼叫底層函式。如果您只需要一個封閉,它會使用與封閉接受的參數相同的參數呼叫命名函式,請勿手動將呼叫包裝在 lambda 中。

gooddart
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);
baddart
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 中最佳使用變數。

請遵循局部變數的 varfinal 的一致規則

#

大多數局部變數不應該有型別註解,且應該僅使用 varfinal 宣告。有兩個廣泛使用的規則,說明何時使用其中一個

  • 對未重新指派的局部變數使用 final,對已重新指派的局部變數使用 var

  • 對所有局部變數使用 var,即使是未重新指派的變數也是如此。切勿對局部變數使用 final。(當然,仍鼓勵對欄位和頂層變數使用 final。)

任一規則都可以接受,但請選擇一個,並在整個程式碼中一致地套用它。這樣,當讀者看到 var 時,他們就知道它是否表示變數稍後會在函式中指派。

避免儲存可計算的內容

#

在設計類別時,您通常希望將多個檢視公開到相同的底層狀態。您經常會看到在建構函式中計算所有這些檢視的程式碼,然後儲存它們

baddart
class Circle {
  double radius;
  double area;
  double circumference;

  Circle(double radius)
      : radius = radius,
        area = pi * radius * radius,
        circumference = pi * 2.0 * radius;
}

此程式碼有兩個問題。首先,它可能會浪費記憶體。嚴格來說,面積和圓周率是快取。它們是儲存的計算,我們可以從現有的其他資料重新計算。它們以增加的記憶體來換取減少的 CPU 使用率。我們知道我們有值得進行此權衡的效能問題嗎?

更糟的是,此程式碼是錯誤的。快取的問題在於無效化—您如何知道快取已過時且需要重新計算?在此,我們從未執行此動作,即使 radius 是可變的。您可以指定不同的值,而 areacircumference 會保留其先前的值,現在這些值不正確。

若要正確處理快取無效化,我們需要執行此動作

baddart
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;
  }
}

這是大量需要撰寫、維護、除錯和閱讀的程式碼。相反地,您的第一個實作應該是

gooddart
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# 中的屬性)之後。這樣一來,如果您需要在這些成員中執行更多工作,您就可以執行,而不需要變更呼叫位置。這是因為呼叫 getter 方法與在 Java 中存取欄位不同,而存取屬性與在 C# 中存取原始欄位不相容。

Dart 沒有這個限制。欄位和 getter/setter 完全無法區分。您可以在類別中公開一個欄位,然後在不觸及使用該欄位的任何程式碼的情況下,將其封裝在 getter 和 setter 中。

gooddart
class Box {
  Object? contents;
}
baddart
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

優先使用 final 欄位來建立唯讀屬性

#

如果您有一個欄位,外部程式碼應該能夠看到但不能指定,一個在許多情況下可行的簡單解決方案就是簡單地將其標記為 final

gooddart
class Box {
  final contents = [];
}
baddart
class Box {
  Object? _contents;
  Object? get contents => _contents;
}

當然,如果您需要在建構函式外部對欄位進行內部指定,您可能需要執行「私人欄位、公開 getter」模式,但不要在需要之前就使用它。

考慮對簡單成員使用 =>

#

Linter 規則:prefer_expression_function_bodies

除了對函式表達式使用 => 之外,Dart 也允許您使用它定義成員。這種樣式非常適合只計算並傳回值的簡單成員。

gooddart
double get area => (right - left) * (bottom - top);

String capitalize(String name) =>
    '${name[0].toUpperCase()}${name.substring(1)}';

撰寫 程式碼的人似乎很喜歡 =>,但很容易濫用它,最後得到難以閱讀的程式碼。如果您的宣告超過幾行,或包含深度巢狀表達式(串接和條件運算子是常見的違規者),請為自己和所有必須閱讀您的程式碼的人一個好處,使用區塊主體和一些陳述式。

gooddart
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;
}
baddart
Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
    ? null
    : _opened[chest] = (Treasure(where)..addAll(chest.contents));

您也可以對不傳回值的成員使用 =>。當一個 setter 很小,並有一個使用 => 的對應 getter 時,這是慣用的用法。

gooddart
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.。一個是當具有相同名稱的局部變數遮蔽您想要存取的成員時

baddart
class Box {
  Object? value;

  void clear() {
    this.update(null);
  }

  void update(Object? value) {
    this.value = value;
  }
}
gooddart
class Box {
  Object? value;

  void clear() {
    update(null);
  }

  void update(Object? value) {
    this.value = value;
  }
}

使用 this. 的另一個時機是重新導向到命名建構函式

baddart
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // This won't parse or compile!
  // ShadeOfGray.alsoBlack() : black();
}
gooddart
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // But now it will!
  ShadeOfGray.alsoBlack() : this.black();
}

請注意,建構函式參數絕不會在建構函式初始化清單中遮蔽欄位

gooddart
class Box extends BaseBox {
  Object? value;

  Box(Object? value)
      : value = value,
        super(value);
}

這看起來令人驚訝,但會按照您的期望運作。幸運的是,由於初始化形式參數和 super 初始化項,這種程式碼相對罕見。

請在可能的情況下於宣告時初始化欄位

#

如果一個欄位不依賴任何建構函式參數,則可以在其宣告中初始化,而且應該這麼做。當類別有多個建構函式時,它需要較少的程式碼並避免重複。

baddart
class ProfileMark {
  final String name;
  final DateTime start;

  ProfileMark(this.name) : start = DateTime.now();
  ProfileMark.unnamed()
      : name = '',
        start = DateTime.now();
}
gooddart
class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

有些欄位無法在宣告中初始化,因為它們需要參照 this,例如使用其他欄位或呼叫方法。但是,如果欄位標記為 late,則初始化項可以存取 this

當然,如果一個欄位依賴建構函式參數,或由不同的建構函式以不同的方式初始化,則此準則不適用。

建構函式

#

下列最佳實務適用於為類別宣告建構函式。

請在可能的情況下使用初始化形式

#

Linter 規則:prefer_initializing_formals

許多欄位會直接從建構函式參數初始化,例如

baddart
class Point {
  double x, y;
  Point(double x, double y)
      : x = x,
        y = y;
}

我們必須在此處輸入 x 次才能定義一個欄位。我們可以做得更好

gooddart
class Point {
  double x, y;
  Point(this.x, this.y);
}

建構函數參數之前的這個 this. 語法稱為「初始化形式」。你無法總是利用它。有時候你想要有一個命名參數,其名稱與你正在初始化的欄位名稱不符。但是當你可以使用初始化形式時,你應該使用。

當建構函數初始化器清單可以執行時,請勿使用 late

#

Dart 要求你在讀取非可為空欄位之前初始化它們。由於欄位可以在建構函數主體內讀取,這表示如果你在主體執行之前沒有初始化非可為空欄位,你會收到錯誤訊息。

你可以透過將欄位標記為 late 來消除這個錯誤。如果你在初始化欄位之前存取它,這會將編譯時期錯誤轉換為執行時期錯誤。在某些情況下,這是你需要的,但通常正確的修正方式是在建構函數初始化器清單中初始化欄位

gooddart
class Point {
  double x, y;
  Point.polar(double theta, double radius)
      : x = cos(theta) * radius,
        y = sin(theta) * radius;
}
baddart
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 建構函數的要求。)

gooddart
class Point {
  double x, y;
  Point(this.x, this.y);
}
baddart
class Point {
  double x, y;
  Point(this.x, this.y) {}
}

請勿使用 new

#

Linter 規則:unnecessary_new

呼叫建構函數時,new 關鍵字是可選的。它的意義不明確,因為工廠建構函數表示 new 呼叫實際上可能不會傳回新的物件。

這門語言仍然允許使用 new,但請將它視為已棄用,並避免在你的程式碼中使用它。

gooddart
Widget build(BuildContext context) {
  return Row(
    children: [
      RaisedButton(
        child: Text('Increment'),
      ),
      Text('Click!'),
    ],
  );
}
baddart
Widget build(BuildContext context) {
  return new Row(
    children: [
      new RaisedButton(
        child: new Text('Increment'),
      ),
      new Text('Click!'),
    ],
  );
}

請勿重複使用 const

#

Linter 規則:unnecessary_const

在表達式必須為常數的內容中,const 關鍵字是隱含的,不需要寫,也不應該寫。這些內容是任何內部的表達式

  • 常數集合文字。
  • 常數建構函呼叫
  • 元資料註解。
  • 常數變數宣告的初始化程式。
  • switch case 表達式—case 之後的部份,: 之前的部份,而不是 case 的主體。

(預設值未包含在此清單中,因為 Dart 的未來版本可能會支援非常數預設值。)

基本上,在任何可以將 new 寫成 const 的地方,Dart 都允許您省略 const

gooddart
const primaryColors = [
  Color('red', [255, 0, 0]),
  Color('green', [0, 255, 0]),
  Color('blue', [0, 0, 255]),
];
baddart
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 子句的捕捉

#

Linter 規則:avoid_catches_without_on_clauses

沒有 on 限定詞的捕捉子句會捕捉 try 區塊中程式碼擲回的 任何 東西。寶可夢例外處理 很可能不是您想要的。您的程式碼是否正確處理 StackOverflowErrorOutOfMemoryError?如果您錯誤地將錯誤的引數傳遞給該 try 區塊中的方法,您希望您的偵錯工具指出錯誤,還是希望有用的 ArgumentError 被吞掉?您是否希望該程式碼中的任何 assert() 陳述式有效消失,因為您正在捕捉擲回的 AssertionError

答案可能是「否」,在這種情況下,您應該篩選您捕捉的類型。在大多數情況下,您應該有一個 on 子句,將您限制在您知道且正確處理的執行時期失敗類型。

在極少數情況下,您可能希望捕捉任何執行時期錯誤。這通常出現在框架或低階程式碼中,這些程式碼會嘗試隔離任意應用程式程式碼,以避免造成問題。即使在此,捕捉 Exception 通常優於捕捉所有類型。Exception 是所有 執行時期 錯誤的基本類別,且排除表示程式碼中 程式化 錯誤的錯誤。

不要捨棄沒有 on 子句的捕捉中的錯誤

#

如果您真的覺得您需要捕捉程式碼區域中可以擲回的 所有 內容,請 對您捕捉到的內容執行某項動作。記錄它、顯示給使用者或重新擲回它,但不要靜默捨棄它。

只為程式化錯誤擲回實作 Error 的物件

#

Error 類別是 程式化 錯誤的基本類別。當該類型或其子介面之一(例如 ArgumentError)的物件被擲回時,表示您的程式碼中有一個 錯誤。當您的 API 要向呼叫者報告其使用不正確時,擲回 Error 會清楚地傳送該訊號。

相反地,如果例外是某種執行時期失敗,不會表示程式碼中的錯誤,則擲回 Error 會令人誤解。請改為擲回其中一個核心 Exception 類別或其他類型。

不要明確捕捉 Error 或實作它的類型

#

Linter 規則:avoid_catching_errors

這是從上述內容得來的。由於 Error 表示程式碼中的錯誤,因此它應該解開整個呼叫堆疊、停止程式,並印出堆疊追蹤,以便您可以找到並修正錯誤。

捕捉這些類型的錯誤會中斷該處理程序並隱藏錯誤。與事後新增錯誤處理程式碼來處理此例外情況不同,請返回並修正導致它在第一時間被拋出的程式碼。

請使用 rethrow 來重新拋出已捕捉的例外情況

#

Linter 規則:use_rethrow_when_possible

如果您決定重新拋出例外情況,請優先使用 rethrow 陳述式,而不是使用 throw 拋出相同的例外情況物件。rethrow 會保留例外情況的原始堆疊追蹤。另一方面,throw 會將堆疊追蹤重設為最後拋出的位置。

baddart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) throw e;
  handle(e);
}
gooddart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}

非同步

#

Dart 有多項語言功能支援非同步程式設計。下列最佳實務適用於非同步編碼。

優先使用 async/await,而非使用原始 future

#

即使使用像 futures 這樣的出色抽象化,非同步程式碼也出了名的難以閱讀和除錯。async/await 語法改進了可讀性,並讓您在非同步程式碼中使用所有 Dart 控制流程結構。

gooddart
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;
  }
}
baddart
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,請這麼做。

gooddart
Future<int> fastestBranch(Future<int> left, Future<int> right) {
  return Future.any([left, right]);
}
baddart
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
  return Future.any([left, right]);
}

async 用的情況包括

  • 您正在使用 await。(這是顯而易見的。)

  • 您正在非同步地傳回錯誤。async 然後 throwreturn Future.error(...) 短。

  • 您正在傳回一個值,並且希望它隱式地包裝在一個 future 中。asyncFuture.value(...) 短。

gooddart
Future<void> usesAwait(Future<String> later) async {
  print(await later);
}

Future<void> asyncError() async {
  throw 'Error!';
}

Future<String> asyncValue() async => 'value';

考慮使用高階方法來轉換串流

#

這與上述關於可迭代項目的建議相符。串流支援許多相同的方法,並也能正確處理傳輸錯誤、關閉等事項。

避免直接使用 Completer

#

許多剛接觸非同步程式設計的人,都想要撰寫產生 future 的程式碼。Future 中的建構函式似乎不符合他們的需求,因此他們最後會找到 Completer 類別並使用它。

baddart
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(),因為它們較為清楚且能更輕鬆地處理錯誤。

gooddart
Future<bool> fileContainsBear(String path) {
  return File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
gooddart
Future<bool> fileContainsBear(String path) async {
  var contents = await File(path).readAsString();
  return contents.contains('bear');
}

在消除歧義時,針對 Future<T> 測試 FutureOr<T>,其類型引數可能是 Object

#

在對 FutureOr<T> 執行任何有用的操作之前,通常需要執行 is 檢查,以查看您擁有 Future<T> 還是單純的 T。如果類型引數是特定類型,例如 FutureOr<int>,則使用哪個測試(is intis Future<int>)並不重要。這兩個測試都可以,因為這兩個類型是相異的。

但是,如果值類型是 Object 或可能使用 Object 進行實例化的類型參數,則兩個分支會重疊。Future<Object> 本身實作 Object,因此 is Objectis T(其中 T 是可能使用 Object 進行實例化的類型參數)即使物件是 future 也會傳回 true。相反地,請明確測試 Future 案例

gooddart
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;
  }
}
baddart
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> 給它,它會錯誤地將其視為單純的同步值。