內容

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 字串

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

而不是函式庫名稱

不好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

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

Dart 認為這些是引入兩個完全不相關的函式庫。為了避免混淆 Dart 和您自己,請遵循這兩項規則

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

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

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

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

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

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

  return bestItem;
}
不好dart
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 作為預設值,因此不需要寫入。

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

請勿在相等運算中使用 truefalse

#

使用相等運算子針對布林值字面值評估一個不可為 null 的布林值表達式是多餘的。總是可以使用刪除相等運算子,並在必要時使用一元否定運算子 ! 來簡化。

dart
if (nonNullableBool) { ... }

if (!nonNullableBool) { ... }
不好dart
if (nonNullableBool == true) { ... }

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

若要評估一個可為 null 的布林值表達式,您應該使用 ?? 或明確的 != null 檢查。

dart
// 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) { ... }
不好dart
// 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 變數。

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

dart
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 時都使用 ! 更乾淨、更安全。

不好dart
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++ 中一樣,只需將它們彼此相鄰放置即可完成。這是建立一個不適合放在一行上的單個長字串的好方法。

dart
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');
不好dart
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 中確實可行,但使用內插法通常更乾淨、更簡短。

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

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

避免在不需要時在插值中使用大括號

#

Linter 規則:unnecessary_brace_in_string_interps

如果您內插的是一個簡單的識別項,且後方並未緊接著其他字母數字文字,則應省略 {}

dart
var greeting = 'Hi, $name! I love your ${decade}s costume.';
不好dart
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';

集合

#

Dart 開箱即支援四種集合類型:列表、對應、佇列和集合。以下最佳做法適用於集合。

盡可能使用集合字面值

#

Linter 規則:prefer_collection_literals

Dart 有三種核心集合類型:List、Map 和 Set。Map 和 Set 類別像大多數類別一樣具有未命名的建構函式。但由於這些集合的使用頻率很高,Dart 具有更簡潔的內建語法來建立它們。

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

請注意,此準則不適用於這些類別的已命名建構函式。List.from()Map.fromIterable() 和相關的函式都有其用途。(List 類別也有未命名的建構函式,但在 null 安全 Dart 中是禁止的。)

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

dart
var arguments = [
  ...options,
  command,
  ...?modeFlags,
  for (var path in filePaths)
    if (path.endsWith('.dart')) path.replaceAll('.dart', '.js')
];
不好dart
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。使用不需要您否定結果的 getter。

dart
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
不好dart
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 中,如果您想要在序列上迭代,則慣用的方法是使用迴圈。

dart
for (final person in people) {
  ...
}
不好dart
people.forEach((person) {
  ...
});

請注意,此準則特別說明「函式字面值」。如果您想要在每個元素上調用一些已存在的函式,則 forEach() 就可以。

dart
people.forEach(print);

另請注意,使用 Map.forEach() 始終是可以的。對應不可迭代,因此此準則不適用。

除非您打算變更結果的類型,否則請勿使用 List.from()

#

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

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

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

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

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

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

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

dart
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()

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

這很冗長,但更糟糕的是,它傳回的可迭代物件的類型可能不是您想要的。在此範例中,它會傳回 Iterable<Object>,即使您可能想要 Iterable<int>,因為這是您要篩選到的類型。

有時您會看到程式碼透過新增 cast() 來「更正」上述錯誤

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

這很冗長,並且會建立兩個包裝函式,具有兩個間接層級和多餘的執行階段檢查。幸好,核心程式庫具有whereType() 方法,適用於此精確的使用案例

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

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

當附近的操作可以執行時,請勿使用 cast()

#

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

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

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

如果您呼叫 map(),請為它提供明確的類型引數,以便它產生所需類型的可迭代物件。類型推斷通常會根據您傳遞至 map() 的函式為您挑選正確的類型,但有時您需要明確指定。

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

請避免使用 cast()

#

這是前一條規則的較寬鬆概括。有時,您無法使用附近的操作來修正某些物件的類型。即使如此,也請盡可能避免使用 cast() 來「變更」集合的類型。

請改用下列任何選項

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

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

  • 使用 List.from() 進行急切轉換。如果您最終將存取集合中的大多數元素,且您不需要物件由原始的即時物件支援,請使用 List.from() 來轉換它。

    cast() 方法會回傳一個惰性集合,該集合會在每次操作時檢查元素類型。如果您只對少數元素執行少量操作,這種惰性可能會有好處。但在許多情況下,惰性驗證和包裝的開銷會超過其優點。

以下範例示範如何使用正確的類型建立集合:

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

以下示範如何在存取時轉換每個元素:

dart
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);
  }
}
不好dart
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() 進行急切轉換:

dart
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];
}
不好dart
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 繫結到變數。

dart
void main() {
  void localFunction() {
    ...
  }
}
不好dart
void main() {
  var localFunction = () {
    ...
  };
}

當 tear-off 可以完成時,不要建立 lambda

#

程式碼檢查規則:unnecessary_lambdas

當您在沒有括號的情況下參照函式、方法或具名建構函式時,Dart 會建立一個剝離。這是一個閉包,它會採用與函式相同的參數,並在您呼叫它時呼叫底層函式。如果您的程式碼需要一個閉包,該閉包會以與閉包接受的參數相同的參數呼叫具名函式,則不要將呼叫包裝在 lambda 中。請使用剝離。

dart
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);
不好dart
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 時,他們就知道這是否表示該變數會在函式中稍後指派。

避免儲存您可以計算的內容

#

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

不好dart
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 將會保留其先前、現在不正確的值。

為了正確處理快取失效,我們需要這麼做

不好dart
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;
  }
}

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

dart
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 中,而無需接觸任何使用該欄位的程式碼。

dart
class Box {
  Object? contents;
}
不好dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

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

#

如果您有一個外部程式碼應可看見但不能指派給的欄位,在許多情況下,簡單的解決方案是直接將其標示為 final

dart
class Box {
  final contents = [];
}
不好dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
}

當然,如果您需要在建構函式外部在內部指派給該欄位,您可能需要執行「私有欄位、公有 getter」模式,但請在需要時才執行此操作。

請考慮對簡單的成員使用 =>

#

程式碼檢查規則:prefer_expression_function_bodies

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

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

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

編寫程式碼的人似乎喜歡 =>,但它非常容易被濫用,最終導致程式碼難以閱讀。如果您的宣告超過幾行或包含深度巢狀的表達式(串聯和條件運算子是常見的罪魁禍首),請為您自己和所有必須閱讀您的程式碼的人著想,使用區塊主體和一些陳述式。

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

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

dart
num get x => center.x;
set x(num value) => center = Point(value, center.y);

除非是為了重新導向至具名建構函式或避免遮蔽,否則請勿使用 this.

#

程式碼檢查規則:unnecessary_this

JavaScript 需要明確的 this.,才能參照目前正在執行其方法的物件上的成員,但 Dart(如 C++、Java 和 C#)沒有這種限制。

只有在兩種情況下才需要使用 this.。其中一種情況是,當具有相同名稱的區域變數遮蔽您想要存取的成員時

不好dart
class Box {
  Object? value;

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

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

  void clear() {
    update(null);
  }

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

另一種使用 this. 的情況是,重新導向至具名建構函式時

不好dart
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

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

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

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

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

dart
class Box extends BaseBox {
  Object? value;

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

這看起來很令人驚訝,但其運作方式如您所願。幸運的是,由於初始化形式參數和 super 初始化工具,這種程式碼相對罕見。

盡可能在宣告時初始化欄位

#

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

不好dart
class ProfileMark {
  final String name;
  final DateTime start;

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

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

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

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

建構子

#

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

盡可能使用初始化形式參數

#

程式碼檢查規則:prefer_initializing_formals

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

不好dart
class Point {
  double x, y;
  Point(double x, double y)
      : x = x,
        y = y;
}

我們必須在這裡輸入 x 次才能定義欄位。我們可以做得更好

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

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

當建構函式初始化清單可以完成工作時,請勿使用 late

#

Dart 要求您先初始化非可為 null 的欄位,然後才能讀取它們。由於可以在建構函式主體內讀取欄位,這表示如果您在主體執行之前未初始化非可為 null 的欄位,就會收到錯誤。

您可以將欄位標示為 late 來消除此錯誤。如果您在初始化之前存取該欄位,則會將編譯時期錯誤變成執行階段錯誤。這是您在某些情況下需要的,但正確的修正通常是在建構函式初始化清單中初始化欄位

dart
class Point {
  double x, y;
  Point.polar(double theta, double radius)
      : x = cos(theta) * radius,
        y = sin(theta) * radius;
}
不好dart
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 建構函式是必要的。)

dart
class Point {
  double x, y;
  Point(this.x, this.y);
}
不好dart
class Point {
  double x, y;
  Point(this.x, this.y) {}
}

請勿使用 new

#

程式碼檢查規則:unnecessary_new

呼叫建構函式時,new 關鍵字是選用的。它的含義不明確,因為 factory 建構函式表示 new 叫用可能不會實際回傳新的物件。

該語言仍然允許 new,但請將其視為已淘汰並避免在您的程式碼中使用。

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      RaisedButton(
        child: Text('Increment'),
      ),
      Text('Click!'),
    ],
  );
}
不好dart
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

dart
const primaryColors = [
  Color('red', [255, 0, 0]),
  Color('green', [0, 255, 0]),
  Color('blue', [0, 0, 255]),
];
不好dart
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 區塊中程式碼擲回的任何項目。寶可夢例外狀況處理很可能不是您想要的。您的程式碼是否正確處理了 StackOverflowErrorOutOfMemoryError?如果您在該 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 會將堆疊追蹤重置到最後拋出的位置。

不好dart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) throw e;
  handle(e);
}
dart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}

非同步

#

Dart 具有多個語言功能來支援非同步程式設計。以下最佳實務適用於非同步程式碼。

優先使用 async/await 而不是使用原始 futures

#

非同步程式碼眾所周知難以閱讀和除錯,即使使用像 futures 這樣好的抽象概念也是如此。async/await 語法提高了可讀性,並讓您可以在非同步程式碼中使用所有 Dart 控制流程結構。

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;
  }
}
不好dart
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,請這樣做。

dart
Future<int> fastestBranch(Future<int> left, Future<int> right) {
  return Future.any([left, right]);
}
不好dart
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
  return Future.any([left, right]);
}

async用的情況包括

  • 您正在使用 await。(這是明顯的情況。)

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

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

dart
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 類別並使用它。

不好dart
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(),因為它們更清晰且使錯誤處理更容易。

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

當消除類型引數可能為 ObjectFutureOr<T> 的歧義時,請測試 Future<T>

#

在您可以對 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 的情況

dart
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;
  }
}
不好dart
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>,它會錯誤地將其視為裸的同步值。