跳到主要內容
目錄

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 指令使用函式庫的名稱。命名函式庫是一項舊版功能,現在已不建議使用。當判斷部件屬於哪個函式庫時,函式庫名稱可能會引入歧義。

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

my_library.dart
dart
library my_library;

part 'some/other/file.dart';

則部件檔案應使用函式庫檔案的 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 的機會時,才是正確的,但當它可能產生 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 變數。

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。

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 時都使用 ! 更清晰且更安全

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 有三種核心集合類型:List、Map 和 Set。Map 和 Set 類別具有未命名的建構子,就像大多數類別一樣。但是,由於這些集合的使用頻率非常高,因此 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() 和朋友都有其用途。(List 類別也有未命名的建構子,但在空值安全 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,有兩種明顯的方法可以產生包含相同元素的新 List

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);

但如果您的目標只是複製可迭代物件並保留其原始類型,或者您不關心類型,則請使用 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()

#

通常,當您處理可迭代物件或 stream 時,您會對其執行多個轉換。最後,您想要產生具有特定類型引數的物件。與其附加對 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 = () {
    ...
  };
}

當可以使用 tear-off 時,請勿建立 lambda

#

Linter 規則:unnecessary_lambdas

當您在沒有括號的情況下參考函式、方法或具名建構子時,Dart 會建立tear-off。這是一個閉包,它採用與函式相同的參數,並在您呼叫它時叫用基礎函式。如果您的程式碼需要一個閉包,該閉包使用與閉包接受的參數相同的參數來叫用具名函式,請勿將呼叫包裝在 lambda 中。請使用 tear-off。

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# 中的屬性)之後。這樣一來,如果您將來需要在這些成員中執行更多工作,您可以做到,而無需修改呼叫點。這是因為在 Java 中呼叫 getter 方法與存取欄位不同,而且在 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);
}

這看起來令人驚訝,但運作方式符合您的期望。幸運的是,由於初始化形式參數和父類別初始化器,像這樣的程式碼相對罕見。

請務必在可能時於宣告時初始化欄位

#

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

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 關鍵字是隱含的,不需要寫出來,也不應該寫出來。這些上下文是內部的任何表達式

  • const 集合字面值。
  • const 建構子呼叫
  • 中繼資料註解。
  • const 變數宣告的初始化器。
  • switch case 表達式—case 之後到 : 之前的部分,而不是 case 的主體。

(預設值不包含在此列表中,因為未來版本的 Dart 可能會支援非 const 預設值。)

基本上,在任何寫 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 子句的 catch

#

Linter 規則: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 會將堆疊追蹤重設為最後拋出的位置。

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

非同步

#

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

相較於使用原始 futures,偏好使用 async/await

#

即使使用像 future 這樣好的抽象概念,非同步程式碼也以難以閱讀和除錯而聞名。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';

考量使用高階方法來轉換 stream

#

這與上面關於可迭代物件的建議類似。Stream 支援許多相同的方法,並且也正確地處理傳輸錯誤、關閉等事項。

避免直接使用 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');
}

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

#

在您可以對 FutureOr<T> 執行任何有用的操作之前,您通常需要執行 is 檢查,以查看您是否擁有 Future<T> 或裸露的 T。如果類型引數是像 FutureOr<int> 這樣的特定類型,則您使用哪個測試並不重要,is intis Future<int>。兩者都有效,因為這兩種類型是不相交的。

但是,如果值類型是 Object 或類型參數,該類型參數可能以 Object 實例化,則兩個分支重疊。Future<Object> 本身實作 Object,因此即使物件是 future,is Objectis T(其中 T 是可能以 Object 實例化的某些類型參數)也會傳回 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>,它會錯誤地將其視為裸露的、同步的值。