目錄

高效 Dart 語言指南:用法範例

目錄 keyboard_arrow_down keyboard_arrow_up
more_horiz

每天在你寫的 Dart 程式碼中都會應用到這些準則。庫的使用者可能不需要知道你在其中的一些想法,但是維護者肯定是需要的。

這些準則可以幫助你在多個檔案編寫程式的情況下保證一致性和可維護性。為了讓準則簡潔,這裡使用“import”來同時代表 importexport 。準則同時適用於這兩者。

DO use strings in part of directives

Linter rule: use_string_in_part_of_directives

part of 中使用字串

很多 Dart 開發者會避免直接使用 part 。他們發現當庫僅有一個檔案的時候很容易讀懂程式碼。如果你確實要使用 part 將庫的一部分拆分為另一個檔案,則 Dart 要求另一個檔案指示它所屬庫的路徑。

由於遺留原因,Dart 允許 part of 指令使用它所屬的函式庫的 名稱。這使得工具很難直接查詢到這個檔案對應主庫檔案,使得庫和檔案之間的關係模糊不清。

推薦的現代語法是使用 URI 字串直接指向庫檔案。首選的現代語法是使用直接指向庫檔案的URI字串,URI 的使用和其他指令中一樣。如果你有一些函式庫,my_library.dart,其中包含:

library my_library;

part 'some/other/file.dart';

從庫中拆分的檔案應該如下所示:

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

而不是:

part of my_library;

不要 匯入 package 中 src 目錄下的函式庫

Linter rule: implementation_imports

lib 下的 src 目錄 被指定 為 package 自己實現的私有庫。基於包維護者對版本的考慮,package 使用了這種約定。在不破壞 package 的情況下,維護者可以自由地對 src 目錄下的程式碼進行修改。

這意味著,你如果匯入了其中的私有庫,按理論來講,一個不破壞 package 的次版本就會影響到你的程式碼。

DON’T allow an import path to reach into or out of lib

Linter rule: avoid_relative_lib_imports

A package: import lets you access a library inside a package’s lib directory without having to worry about where the package is stored on your computer. For this to work, you cannot have imports that require the lib to be in some location on disk relative to other files. In other words, a relative import path in a file inside lib can’t reach out and access a file outside of the lib directory, and a library outside of lib can’t use a relative path to reach into the lib directory. Doing either leads to confusing errors and broken programs.

For example, say your directory structure looks like this:

my_package
└─ lib
   └─ api.dart
   test
   └─ api_test.dart

And say api_test.dart imports api.dart in two ways:

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

Dart thinks those are imports of two completely unrelated libraries. To avoid confusing Dart and yourself, follow these two rules:

  • Don’t use /lib/ in import paths.
  • Don’t use ../ to escape the lib directory.

Instead, when you need to reach into a package’s lib directory (even from the same package’s test directory or any other top-level directory), use a package: import.

import 'package:my_package/api.dart';

A package should never reach out of its lib directory and import libraries from other places in the package.

PREFER relative import paths

Linter rule: prefer_relative_imports

比如,下面是你的 package 目錄結構:

my_package
└─ lib
   ├─ src
   │  └─ stuff.dart
   │  └─ utils.dart
   └─ api.dart
   test
   │─ api_test.dart
   └─ test_utils.dart

Here is how the various libraries should import each other:

如果 api.dart 想匯入 utils.dart ,應該這樣使用:

import 'src/stuff.dart';
import 'src/utils.dart';

lib/src/utils.dart:

import '../api.dart';
import 'stuff.dart';

test/api_test.dart:

import 'package:my_package/api.dart'; // Don't reach into 'lib'.

import 'test_utils.dart'; // Relative within 'test' is fine.

Null

DON’T explicitly initialize variables to null

Linter rule: avoid_init_to_null

If a variable has a non-nullable type, Dart reports a compile error if you try to use it before it has been definitely initialized. If the variable is nullable, then it is implicitly initialized to null for you. There’s no concept of “uninitialized memory” in Dart and no need to explicitly initialize a variable to null to be “safe”.

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

DON’T use an explicit default value of null

Linter rule: avoid_init_to_null

If you make a nullable parameter optional but don’t give it a default value, the language implicitly uses null as the default, so there’s no need to write it.

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

DON’T use true or false in equality operations

Using the equality operator to evaluate a non-nullable boolean expression against a boolean literal is redundant. It’s always simpler to eliminate the equality operator, and use the unary negation operator ! if necessary:

if (nonNullableBool) { ... }

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

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

To evaluate a boolean expression that is nullable, you should use ?? or an explicit != null check.

// 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 is a viable expression, but shouldn’t be used for several reasons:

  • It doesn’t indicate the code has anything to do with null.

  • Because it’s not evidently null related, it can easily be mistaken for the non-nullable case, where the equality operator is redundant and can be removed. That’s only true when the boolean expression on the left has no chance of producing null, but not when it can.

  • The boolean logic is confusing. If nullableBool is null, then nullableBool == true means the condition evaluates to false.

The ?? operator makes it clear that something to do with null is happening, so it won’t be mistaken for a redundant operation. The logic is much clearer too; the result of the expression being null is the same as the boolean literal.

Using a null-aware operator such as ?? on a variable inside a condition doesn’t promote the variable to a non-nullable type. If you want the variable to be promoted inside the body of the if statement, it’s better to use an explicit != null check instead of ??.

AVOID late variables if you need to check whether they are initialized

Dart offers no way to tell if a late variable has been initialized or assigned to. If you access it, it either immediately runs the initializer (if it has one) or throws an exception. Sometimes you have some state that’s lazily initialized where late might be a good fit, but you also need to be able to tell if the initialization has happened yet.

Although you could detect initialization by storing the state in a late variable and having a separate boolean field that tracks whether the variable has been set, that’s redundant because Dart internally maintains the initialized status of the late variable. Instead, it’s usually clearer to make the variable non-late and nullable. Then you can see if the variable has been initialized by checking for null.

Of course, if null is a valid initialized value for the variable, then it probably does make sense to have a separate boolean field.

CONSIDER assigning a nullable field to a local variable to enable type promotion

Checking that a nullable variable is not equal to null promotes the variable to a non-nullable type. That lets you access members on the variable and pass it to functions expecting a non-nullable type. Unfortunately, promotion is only sound for local variables and parameters, so fields and top-level variables aren’t promoted.

One pattern to work around this is to assign the field’s value to a local variable. Null checks on that variable do promote, so you can safely treat it as non-nullable.

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).';
  }
}

Assigning to a local variable can be cleaner and safer than using ! every place the field or top-level variable is used:

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).';
  }
}

Be careful when using a local variable. If you need to write back to the field, make sure that you don’t write back to the local variable instead. (Making the local variable final can prevent such mistakes.) Also, if the field might change while the local is still in scope, then the local might have a stale value. Sometimes it’s best to simply use ! on the field.

字串

下面是一些需要記住的,關於在 Dart 中使用字串的最佳實踐。

使用相鄰字串的方式連線字面量字串

Linter rule: 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 rule: prefer_interpolation_to_compose_strings

如果你之前使用過其他語言,你一定習慣使用大量 + 將字面量字串以及字串變數連結建構字串。這種方式在 Dart 中同樣有效,但是通常情況下使用插值會更清晰簡短:

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

Note that this guideline applies to combining multiple literals and values. It’s fine to use .toString() when converting only a single object to a string.

避免 在字串插值中使用不必要的大括號

Linter rule: 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 集合中原生支援了四種類型:list, map, queue,和 set。下面是應用於集合的最佳實踐。

儘可能的使用集合字面量

Linter rule: prefer_collection_literals

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

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

Note that this guideline doesn’t apply to the named constructors for those classes. List.from(), Map.fromIterable(), and friends all have their uses. (The List class also has an unnamed constructor, but it is prohibited in null safe Dart.)

Collection literals are particularly powerful in Dart because they give you access to the spread operator for including the contents of other collections, and if and for for performing control flow while building the contents:

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

注意,對於集合類別的 命名 建構函式則不適用上面的規則。 List.from()Map.fromIterable() 都有其使用場景。如果需要一個固定長度的結合,使用 List() 來建立一個固定長度的 list 也是合理的。

不要 使用 .length 來判斷一個集合是否為空

Linter rules: prefer_is_empty, prefer_is_not_empty

Iterable 合約並不要求集合知道其長度,也沒要求在遍歷的時候其長度不能改變。透過呼叫 .length 來判斷集合是否包含內容是非常低效的。

相反,Dart 提供了更加高效率和易用的 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 rule: 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()。Map 是不可迭代的,所以該準則對它無效。

不要 使用 List.from() 除非想修改結果的型別

給定一個可迭代的物件,有兩種常見方式來產生一個包含相同元素的 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 rule: prefer_iterable_whereType

假設你有一個 list 裡面包含了多種型別的物件,但是你指向從它裡面獲取整型型別的資料。那麼你可以像下面這樣使用 where()

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

這個很羅嗦,但是更糟糕的是,它返回的可迭代物件型別可能並不是你想要的。在上面的例子中,雖然你想得到一個 Iterable<int>,然而它返回了一個 Iterable<Object>,這是因為,這是你過濾後得到的型別。

有時候你會看到透過新增 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() 。而不是呼叫 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() 來“改變”集合中元素的型別。

推薦使用下面的方式來替代:

  • 用恰當的型別建立集合。 修改集合被首次建立時的程式碼,為集合提供有一個恰當的型別。

  • 在存取元素時進行 cast 操作。 如果要立即對集合進行迭代,在迭代內部 cast 每個元素。

  • 逼不得已進行 cast,請使用 List.from() 如果最終你會使用到集合中的大部分元素,並且不需要物件還原到原始的物件型別,使用 List.from() 來轉換它。

    cast() 方法返回一個惰性集合 (lazy collection) ,每個操作都會對元素進行檢查。如果只對少數元素執行少量操作,那麼這種惰性方式就非常合適。但在許多情況下,惰性驗證和包裹 (wrapping) 所產生的開銷已經超過了它們所帶來的好處。

下面是 用恰當的型別建立集合 的範例:

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

下面是 在存取元素時進行 cast 操作 的範例:

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() 進行 cast 操作 的範例:

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 rule: prefer_function_declarations_over_variables

現代語言已經意識到本地巢狀(Nesting)函式和閉套件的益處。在一個函式中定義另一個函式非常常見。在許多情況下,這些函式被立即執行並返回結果,而且不需要名字。這種情況下非常適合使用函式表示式來實現。

但是,如果你確實需要給方法一個名字,請使用方法定義而不是把 lambda 賦值給一個變數。

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

不要 使用 lambda 表示式來替代 tear-off

Linter rule: unnecessary_lambdas

如果你參考了一個函式、方法或命名構造,但省略了括號,Dart 會嘗試 tear-off——在呼叫時使用同樣的引數對對應的方法建立閉套件。如果你需要的僅僅是一個參考,請不要利用 lambda 手動包裝。

如果你有一個方法,這個方法呼叫了引數相同的另一個方法。那麼,你不需要人為將這個方法包裝到一個 lambda 表示式中。

var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();

// Function:
charCodes.forEach(print);

// Method:
charCodes.forEach(buffer.write);

// Named constructor:
var strings = charCodes.map(String.fromCharCode);

// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();

// Function:
charCodes.forEach((code) {
  print(code);
});

// Method:
charCodes.forEach((code) {
  buffer.write(code);
});

// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));

// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));

使用 = 來分隔引數名和引數預設值

Linter rule: prefer_equal_for_default_values

在 Dart 3 之前,Dart 同時支援 := 作為引數名和預設值的分隔符。為了與可選的位置引數保持一致,請使用 =

void insert(Object item, {int at = 0}) { ... }
void insert(Object item, {int at: 0}) { ... }

變數

The following best practices describe how to best use variables in Dart.

DO follow a consistent rule for var and final on local variables

Most local variables shouldn’t have type annotations and should be declared using just var or final. There are two rules in wide use for when to use one or the other:

  • Use final for local variables that are not reassigned and var for those that are.

  • Use var for all local variables, even ones that aren’t reassigned. Never use final for locals. (Using final for fields and top-level variables is still encouraged, of course.)

Either rule is acceptable, but pick one and apply it consistently throughout your code. That way when a reader sees var, they know whether it means that the variable is assigned later in the function.

避免 儲存可計算的結果

在設計類別的時候,你常常希望暴露底層狀態的多個表現屬性。常常你會發現在類別的建構函式中計算這些屬性,然後儲存起來:

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

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

上面的程式碼有兩個不妥之處。首先,這樣浪費了記憶體。嚴格來說面積和周長是快取資料。他們儲存的結果可以透過已知的資料計算出來。他們減少了 CPU 消耗卻增加了記憶體消耗。我們還沒有權衡,到底存不存在效能問題?

更糟糕的是,程式碼是錯誤的。問題在於快取是無效的 —— 你如何知道快取何時會過期並且需要重新計算?即便半徑是可變的,在這裡我們也永遠不會這樣做。你可以賦一個不同的值,但面積和周長還是以前的值,現在的值是不正確的。

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

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 rule: 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;
}

當然,如果你需要構造一個內部可以賦值,外部可以存取的欄位,你可以需要這種“私有成員變數,公開存取函式”的模式,但是,如非必要,請不要使用這種模式。

考慮 對簡單成員使用 =>

Linter rule: prefer_expression_function_bodies

除了使用 => 可以用作函式表示式以外, Dart 還允許使用它來定義成員。這種風格非常適合,僅進行計算並返回結果的簡單成員。

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

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

編寫程式碼的人似乎很喜歡 => 語法,但是它很容易被濫用,最後導致程式碼不容易被閱讀。如果你有很多行宣告或包含深層的巢狀(Nesting)表示式(級聯和條件運算子就是常見的罪魁禍首),你以及其他人有誰會願意讀這樣的程式碼!你應該換做使用程式碼塊和一些陳述式來實現。

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 rule: unnecessary_this

JavaScript 需要使用 this. 來參考物件的成員變數,但是 Dart—和 C++, Java, 以及C#—沒有這種限制。

只有當局部變數和成員變數名字一樣的時候,你才需要使用 this. 來存取成員變數。只有兩種情況需要使用 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);
}

這看起來很令人驚訝,但是實際結果是你想要的。幸運的是,由於初始化規則的特殊性,上面的程式碼很少見到。

儘可能的在定義變數的時候初始化變數值

If a field doesn’t depend on any constructor parameters, it can and should be initialized at its declaration. It takes less code and avoids duplication when the class has multiple constructors.

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 = '';
}

Some fields can’t be initialized at their declarations because they need to reference this—to use other fields or call methods, for example. However, if the field is marked late, then the initializer can access this.

當然,對於變數取值依賴建構函式引數的情況以及不同的建構函式取值也不一樣的情況,則不適合本條規則。

建構函式

下面對於類別的建構函式的最佳實踐。

儘可能的使用初始化形式

Linter rule: 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 this. syntax before a constructor parameter is called an “initializing formal”. You can’t always take advantage of it. Sometimes you want to have a named parameter whose name doesn’t match the name of the field you are initializing. But when you can use initializing formals, you should.

DON’T use late when a constructor initializer list will do

Dart 要求你為非空變數在它們被存取前就初始化好內容。如果你沒有初始化,那麼在建構函式執行時就會直接報錯。

如果建構函式引數使用 this. 的方式來初始化欄位,這時引數的型別被認為和欄位型別相同。

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

The initializer list gives you access to constructor parameters and lets you initialize fields before they can be read. So, if it’s possible to use an initializer list, that’s better than making the field late and losing some static safety and performance.

; 來替代空的建構函式體 {}

Linter rule: empty_constructor_bodies

在 Dart 中,沒有具體函式體的建構函式可以使用分號結尾。(事實上,這是不可變建構函式的要求。)

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

不要 使用 new

Linter rule: unnecessary_new

Dart 2 new 關鍵字成為可選項。即使在Dart 1中,其含義也從未明確過,因為在工廠建構函式中,呼叫 new 可能並不意味著一定會返回一個新物件。

為了減少程式碼遷移時的痛苦, Dart 語言仍允許使用 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 rule: unnecessary_const

在表示式一定是常量的上下文中,const 關鍵字是隱含的,不需要寫,也不應該。這裡包括:

  • 一個字面量常量集合。

  • 呼叫一個常量建構函式。

  • 元資料註解。

  • 一個常量宣告的初始化方法。

  • switch case 表示式—— case: 中間的部分,不是 case 執行體。

(預設值並不包含在這個列表中,因為在 Dart 將來的版本中可能會在支援非常量的預設值。)

基本上,任何地方用 new 替代 const 的寫法都是錯的,因為 Dart 2 中允許省略 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 rule: avoid_catches_without_on_clauses

沒有 on 限定的 catch 陳述式會捕獲 try 程式碼塊中丟擲的任何例外。 Pokémon exception handling 可能並不是你想要的。你的程式碼是否正確的處理 StackOverflowError 或者 OutOfMemoryError 例外?如果你使用錯誤的引數呼叫函式,你是期望偵錯程式定位出你的錯誤使用情況還是,把這個有用的 ArgumentError 給吞噬了?由於你捕獲了 AssertionError 例外,導致所有 try 塊內的 assert() 陳述式都失效了,這是你需要的結果嗎?

答案和可能是 “no”,在這種情況下,您應該過濾掉捕獲的型別。在大多數情況下,您應該有一個 on 子句,這樣它能夠捕獲程式在執行時你所關注的限定型別的例外並進行恰當處理。

In rare cases, you may wish to catch any runtime error. This is usually in framework or low-level code that tries to insulate arbitrary application code from causing problems. Even here, it is usually better to catch Exception than to catch all types. Exception is the base class for all runtime errors and excludes errors that indicate programmatic bugs in the code.

不要 丟棄沒有使用 on 陳述式捕獲的例外

如果你真的期望捕獲一段程式碼內的 所有 例外,請在捕獲例外的地方做些事情。記錄下來並顯示給使用者,或者重新丟擲 (rethrow) 例外資訊,記得不要默默的丟棄該例外資訊。

只在代表程式設計錯誤的情況下才丟擲實現了 Error 的例外

Error 類是所有 編碼 錯誤的基底類別。當一個該型別或者其子類別型,例如 ArgumentError 物件被丟擲了,這意味著是你程式碼中的一個 bug。當你的 API 想要告訴呼叫者使用錯誤的時候可以丟擲一個 Error 來表明你的意圖。

同樣的,如果一個例外表示為執行時例外而不是程式碼 bug,則丟擲 Error 則會誤導呼叫者。應該丟擲核心定義的 Exception 類或者其他型別。

不要 顯示的捕獲 Error 或者其子類別

Linter rule: avoid_catching_errors

本條銜接上一條的內容。既然 Error 表示程式碼中的 bug,應該展開整個呼叫堆疊,暫停程式並列印堆疊追蹤,以便找到錯誤並修復。

捕獲這類錯誤打破了處理流程並且程式碼中有 bug。不要在這裡使用錯誤處理程式碼,而是需要到導致該錯誤出現的地方修復你的程式碼。

使用 rethrow 來重新丟擲捕獲的例外

Linter rule: use_rethrow_when_possible

如果你想重新丟擲一個例外,推薦使用 rethrow 陳述式。 rethrow 保留了原來的例外堆疊資訊。而 throw 會把例外堆疊資訊重置為最後丟擲的位置。

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

非同步

Dart 具有幾個語言特性來支援非同步程式設計。下面是針對非同步程式設計的最佳實踐。

推薦 使用 async/await 而不是直接使用底層的特性

顯式的非同步程式碼是非常難以閱讀和除錯的,即使使用很好的抽象(比如 future)也是如此。這就是為何 Dart 提供了 async/await。這樣可以顯著的提高程式碼的可讀性並且讓你可以在非同步程式碼中使用語言提供的所有流程控制陳述式。

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 然後 throwreturn new Future.error(...) 要簡短很多。

  • 你在返回一個值,但是你希望他顯式的使用 Future。asyncFuture.value(...) 要簡短很多。

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

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

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

考慮 使用高階函式來轉換事件流 (stream)

This parallels the above suggestion on iterables. Streams support many of the same methods and also handle things like transmitting errors, closing, etc. correctly.

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

使用 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 的例子:

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> ,它會錯誤地將其視為一個空的同步物件值。