Effective Dart: 最佳實踐

這部分是 Effective Dart 中最重要的內容。 在你的 Dart 代碼中會一直使用這些指導原則。 使用你編寫的庫的用戶可能不太注意到其中的問題, 但是維護你類庫的人一定會發現其中的問題。

  • Strings
    • 要 使用相鄰的字符串字面量定義來鏈接字符串。
    • 推薦 使用插值的形式來組合字符串和值。
    • 避免 在字符串插值中使用多余的大括號。
  • 集合
    • 要 盡可能的使用集合字面量來定義集合。
    • 不要 使用 .length 來判斷集合是否為空。
    • 考慮 使用高階(higher-order)函數來轉換集合數據。
    • 避免 在 Iterable.forEach() 中使用函數聲明形式。
  • 方法(Functions)
    • 要 用方法聲明的形式來給方法起個名字。
    • 不要 使用 lambda 表達式來替代 tear-off。
  • 變量
    • 不要 顯式的把變量初始化為 null。
    • 避免 保存可以計算的結果。
    • 考慮 省略局部變量的類型。
  • 成員
    • 不要 創建沒必要的 getter 和 setter。
    • 推薦 使用 final 關鍵字來限定只讀屬性。
    • 考慮 用 => 來實現只有一個單一返回語句的函數。
    • 不要 使用 this. ,除非遇到了變量沖突的情況。
    • 要 盡可能的在定義變量的時候初始化其值。
  • 構造函數
    • 要 盡可能的使用初始化形式。
    • 不要 在初始化形式上定義類型。
    • 要 用 ; 來替代空函數體的構造函數 {}。
    • 要 把 super() 調用放到構造函數初始化列表之后調用。
  • 錯誤處理
    • 避免 使用沒有 on 語句的 catch。
    • 不要 丟棄沒有使用 on 語句捕獲的異常。
    • 要 只在代表編程錯誤的情況下才拋出實現了 Error 的異常。
    • 不要 顯示的捕獲 Error 或者其子類。
    • 要 使用 rethrow 來重新拋出捕獲的異常。
  • 異步
    • 推薦 使用 async/await 而不是直接使用底層的特性。
    • 不要 在沒有有用效果的情況下使用 async 。
    • 考慮 使用高階函數來轉換事件流(stream)
    • 避免 直接使用 Completer 。

Strings

下面是 Dart 語言中和字符串相關的一些最佳實踐。

使用相鄰的字符串字面量定義來鏈接字符串。

如果有兩個字符串字面量定義—不是變量,而是實際的放到引號內的字符串 —你不用使用 + 來鏈接字符串。和 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.');

推薦 使用插值的形式來組合字符串和值。

如果你之前使用過其他語言,可能會遇到使用大量 + 來組合字符串的情況。 這種情況在 Dart 中也可以使用,但是使用字符串插值會讓代碼看起來 更加簡潔和簡短。

這是正確的示范:

'Hello, $name! You are ${year - birth} years old.';

這是錯誤的示范:

'Hello, ' + name + '! You are ' + (year - birth) + ' years old.';

避免 在字符串插值中使用多余的大括號。

如果求值的只是一個簡單的變量,并且后面沒有緊跟隨在其他字母文本, 則 {} 應該省略。

這是正確的示范:

'Hi, $name!'
"Wear your wildest $decade's outfit."
'Wear your wildest ${decade}s outfit.'

這是錯誤的示范:

'Hi, ${name}!'
"Wear your wildest ${decade}'s outfit."

集合

Dart 天生支持四種集合類型: lists、 maps、 queues、 和 sets。 下面的最佳實踐是針對集合的。

盡可能的使用集合字面量來定義集合。

有兩種方式可以定義一個空的可變的 list:[] 和 new List()。 類似的,有三種方式可以定義一個空的 linked hash map: {}、 new Map()、 和 new LinkedHashMap()。

如果你想創建一個不可變的 list,或者其他自定義類型的集合,你可以使用構造函數。 否則,使用優雅的字面量語法更加合理。 核心庫中暴露這些構造函數易于擴展,但是通常在 Dart 代碼 中并不使用構造函數。

這是正確的示范:

var points = [];
var addresses = {};

這是錯誤的示范:

var points = new List();
var addresses = new Map();

如果有必要還可以提供泛型類型。

這是正確的示范:

var points = <Point>[];
var addresses = <String, Address>{};

這是錯誤的示范:

var points = new List<Point>();
var addresses = new Map<String, Address>();

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

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

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

考慮 使用高階(higher-order)函數來轉換集合數據。

如果你有一個集合并且想要修改里面的內容轉換為另外一個集合, 使用 .map()、 .where() 以及 Iterable 提供的其他函數會 讓代碼更加簡潔。

使用這些函數替代 for 循環會讓代碼更加可以表述你的意圖, 生成一個新的集合系列并不具有副作用。

var aquaticNames = animals
    .where((animal) => animal.isAquatic)
    .map((animal) => animal.name);

如果你串聯或者嵌套調用很多高階函數,則使用 一些命令式代碼可能會 更加清晰。

避免 在 Iterable.forEach() 中使用函數聲明形式。

forEach() 方法通常在 JavaScript 中使用,原因是系統內置的 for-in 循環并不能提供期望的結果。 相反,在 Dart 中如果需要遍歷一個集合,通常使用循環語句。

for (var person in people) {
  ...
}
people.forEach((person) {
  ...
});

如果你只想在每個集合元素上調用一個已經定義好的函數,則可以使用 forEach() 函數。

people.forEach(print);

方法(Functions)

在 Dart 中,方法都是對象。下面是關于調用方法的 一些最佳實踐。

用方法聲明的形式來給方法起個名字。

現代的編程語言都意識到局部嵌套方法以及閉包是非常有用的。 通常是在一個方法中定義另外一個方法。在大部分情況下, 這些嵌套的方法都用作回調函數并且不需要名字。 一個方法表達式非常擅長這種情況。

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

這是正確的示范:

void main() {
  localFunction() {
    ...
  }
}

這是錯誤的示范:

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

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

如果你在一個對象上調用函數并省略了括號, Dart 稱之為 “tear-off”—一個和函數使用同樣參數的閉包,當你調用他的時候就執行 這個函數。

如果你有一個方法使用該方法同樣的參數調用一個函數, 你無需手工的把該函數調用包裝為一個 lambda 表達式。

這是正確的示范:

names.forEach(print);

這是錯誤的示范:

names.forEach((name) {
  print(name);
});

變量

下面的最佳實踐是關于如何在 Dart 中使用變量的。

不要 顯式的把變量初始化為 null。

在 Dart 中沒有初始化的變量和域會自動的 初始化為 null。在語言基本就保證了該行為的可靠性。 在 Dart 中沒有 “未初始化的內存”這個概念。所以添加 = null 是多余的。

int _nextId;

class LazyId {
  int _id;

  int get id {
    if (_nextId == null) _nextId = 0;
    if (_id == null) _id = _nextId++;

    return _id;
  }
}
int _nextId = null;

class LazyId {
  int _id = null;

  int get id {
    if (_nextId == null) _nextId = 0;
    if (_id == null) _id = _nextId++;

    return _id;
  }
}

避免 保存可以計算的結果。

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

這是錯誤的示范:

class Circle {
  num radius;
  num area;
  num circumference;

  Circle(num radius)
      : radius = radius,
        area = math.PI * radius * radius,
        circumference = math.PI * 2.0 * radius;
}

上面的代碼有兩個不妥之處。首先,浪費了內存。 嚴格來說 面積和周長 是緩存對象。他們保存的結果 可以通過已知的數據計算出來。他們主要用來減少 CPU 消耗而增加了內存消耗。 我們是否知道這里有一個需要權衡的性能問題?

更壞的情況是,上面的代碼是 錯的。上面的緩存是 無效的—你如何 知道何時緩存失效了需要重新計算?這里我們無從得知, 但是 radius 確是可變的。你可以給 radius 設置一個不同的值,但是 area 和 circumference 還是之前的值。

為了避免緩存失效,我們需要這樣做:

這是正確的示范:

class Circle {
  num _radius;
  num get radius => _radius;
  set radius(num value) {
    _radius = value;
    _recalculate();
  }

  num _area;
  num get area => _area;

  num _circumference;
  num get circumference => _circumference;

  Circle(this._radius) {
    _recalculate();
  }

  void _recalculate() {
    _area = math.PI * _radius * _radius,
    _circumference = math.PI * 2.0 * _radius;
  }
}

這需要編寫、維護、調試以及閱讀更多的代碼。 如果你一開始這樣寫代碼:

class Circle {
  num radius;

  num get area => math.PI * radius * radius;
  num get circumference => math.PI * 2.0 * radius;

  Circle(this.radius);
}

上面的代碼更加簡潔、使用更少的內存、減少出錯的可能性。只是 保存了盡可能少的數據,這樣無需更新緩存,因為就沒有緩存,面積和周長 是通過計算得來的。

在某些情況下,當計算結果比較費時的時候可能需要緩存,但是只有當你發現 這樣引起性能問題的時候才去緩存它,并且仔細的考慮實現方式并留下 對應的注釋來解釋你所做的優化。

考慮 省略局部變量的類型。

現代的代碼趨勢是保持函數體盡可能的短,而局部變量的類型 通常都可以通過初始化語句推算出來,所以 顯式的定義局部變量類型通常都是制造視覺噪音。

Dart 具有強大的靜態分析工具,可以推斷出局部變量的 類型并且仍然可以提供代碼自動補全以及你所期望的工具支持。

這是正確的示范:

Map<int, List<Person>> groupByZip(Iterable<Person> people) {
  var peopleByZip = <int, List<Person>>{};
  for (var person in people) {
    peopleByZip.putIfAbsent(person.zip, () => <Person>[]);
    peopleByZip[person.zip].add(person);
  }
  return peopleByZip;
}

這是錯誤的示范:

Map<int, List<Person>> groupByZip(Iterable<Person> people) {
  Map<int, List<Person>> peopleByZip = <int, List<Person>>{};
  for (Person person in people) {
    peopleByZip.putIfAbsent(person.zip, () => <Person>[]);
    peopleByZip[person.zip].add(person);
  }
  return peopleByZip;
}

成員

在 Dart 中, 對象的成員可以是 方法(函數)或者 數據(實例變量)。 下面的最佳實踐是關于對象成員的。

不要 創建沒必要的 getter 和 setter。

在Java 和 C# 中通常為了隱藏成員變量而使用一個空的 getter 和 setter 函數。 如果你通過 getter 訪 問成員變量和 直接訪問成員 變量是不一樣的。

Dart 語言沒有這種區別。 成員變量和 getter/setter 是完全一樣的。 你可以一開始暴露一個成員變量,以后再使用 getter 和 setter 來修改 其相關的邏輯,而調用你類的代碼不用做任何修改。

這是正確的示范:

class Box {
  var contents;
}

這是錯誤的示范:

class Box {
  var _contents;
  get contents => _contents;
  set contents(value) {
    _contents = value;
  }
}

推薦 使用 final 關鍵字來限定只讀屬性。

如果你有個變量其他人只能讀取, 而不能修改其值,最簡單的做法就是使用 final 關鍵字來標記這個變量。

這是正確的示范:

class Box {
  final contents = [];
}

這是錯誤的示范:

class Box {
  var _contents;
  get contents => _contents;
}

當然了,如果你確實需要在構造函數以為內部賦值變量的值, 你可以需要這種“私有成員變量,公開訪問函數”的模式, 但是,如非必要,請不要使用這種模式。

考慮 用 => 來實現只有一個單一返回語句的函數。

除了可以使用 => 作為方法表達式以外, Dart 也允許使用其 定義成員函數。對于簡單的計算并返回的情況非常 合適。

get width => right - left;
bool ready(num time) => minTime == null || minTime <= time;
containsValue(String value) => getValues().contains(value);

雖然多行代碼也可以使用 =>,但是為了 表述的簡潔,對于多行代碼還是盡量使用 普通的花括號函數體并使用明顯的 return 語句。

對于 void 類型的成員則 不是 一種期望的使用場景。 讀者期望 => 返回一個有用的值,所以對于沒有返回值的情況,還是使用 { ... } 使代碼更加清晰。

不要 使用 this. ,除非遇到了變量沖突的情況。

JavaScript 需要使用 this. 來引用對象的成員變量,但是 Dart—和 C++, Java, 以及 C#—沒有這種限制。

只有當局部變量和成員變量名字一樣的時候,你才需要使用 this. 來訪問成員變量。

這是錯誤的示范:

class Box {
  var value;

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

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

這才是Dart應該使用的方式:

class Box {
  var value;

  void clear() {
    update(null);
  }

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

注意:構造函數參數在初始化參數列表中從來 不會出現參數沖突的情況。

class Box extends BaseBox {
  var value;

  Box(value)
      : value = value,
        super(value)
      {}
}

上面的代碼看起來有點奇怪,但是其是按照你期望的方式工作的。 幸運的是,由于初始化規則的特殊性,上面的代碼很少見到。

盡可能的在定義變量的時候初始化其值。

如果一個變量不依賴于構造函數中的參數,則應該在定義 變量的時候就初始化其值。這樣可以減少需要的代碼并可以確保 在有多個構造函數的時候你不會忘記初始化該變量。

這是錯誤的示范:

class Folder {
  final String name;
  final List<Document> contents;

  Folder(this.name) : contents = [];
  Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
}

這才是Dart應該使用的方式:

class Folder {
  final String name;
  final List<Document> contents = [];

  Folder(this.name);
  Folder.temp() : name = 'temporary';
}

當然,對于變量取值依賴構造函數參數的情況以及 不同的構造函數取值也不一樣的情況,則不適合本條規則。

構造函數

下面的最佳實踐應用于類的構造函數

盡可能的使用初始化形式。

很多變量都直接使用構造函數參數來初始化,例如:

這是錯誤的示范:

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

為了初始化一個值,我們需要寫四次 x 。我們可以做的更優雅:

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

這里的位于構造函數參數之前的 this. 語法被稱之為 初始化形式(initializing formal)。 有些情況下這無法使用這種形式。特別是,這種形式下無法在 初始化列表中看到變量。 如果能使用該方式就盡量 使用吧。

不要 在初始化形式上定義類型。

如果構造函數使用 this. 來初始化成員變量,則參數的類型 一定是和變量的類型是一樣的。

這是正確的示范:

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

這是錯誤的示范:

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

用 ; 來替代空函數體的構造函數 {}。

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

這是正確的示范:

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

這是錯誤的示范:

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

把 super() 調用放到構造函數初始化列表之后調用。

成員變量初始化是按照他們出現在構造函數初始化列表的順序來初始化的。 如果你把 super() 調用放到初始化列表中間,則 超類的變量初始化會在之類初始化完成之前 調用。

但是這并不意味著超類的構造函數體就會執行。 不管你在何處調用super() , 超類的構造函數只有在所有成員初始化完成后才會執行。 把 super() 放到其他地方則只有讓代碼看起來比較費解。 實際上,DDC 要求其出現在最后。

這是正確的示范:

View(Style style, List children)
    : _children = children,
      super(style) {

這是錯誤的示范:

View(Style style, List children)
    : super(style),
      _children = children {

錯誤處理

Dart 使用異常表示程序出現了錯誤。 下面的最佳實踐是關于如何捕獲和拋出異常的。

避免 使用沒有 on 語句的 catch。

一個沒有 on 限定的 catch 語句會捕獲 try catch 快內的 任何 異常。 Pokémon exception handling 不是你所希望的行為。 你的代碼是否正確的處理 StackOverflowError 或者 OutOfMemoryError 異常了?如果你使用錯誤的參數調用函數, 你是期望調試器定位出你的錯誤使用情況還是 把這個有用的 ArgumentError 給吞噬了? 由于你捕獲了 AssertionError 異常,導致 所有 try 塊內的 assert() 語句都失效了,這是你需要的結果嗎?

答案和可能是 “no”,這種情況下你需要過濾捕獲的列表。 大部分情況下你都需要使用 on 來限定捕獲的具體異常 類型。

在極少數情況下,你可能希望捕獲所有運行時異常。這通常用在框架中 或者底層的代碼中嘗試隔離應用的代碼 來避免產生問題。即使如何,通常 catch Exception 比 catch 所有異常要好。 Exception 是所有 運行時 異常的基類,而不包含 可能是代碼中編碼的 bug 的異常。

不要 丟棄沒有使用 on 語句捕獲的異常。

如果你真的期望捕獲一段代碼內的 所有 異常,請 在捕獲異常的地方做些事情。 記錄下來并顯示給用戶,或者 重新拋出(rethrow)異常信息,記得不要默默的丟棄該異常信息。

只在代表編程錯誤的情況下才拋出實現了 Error 的異常。

Error 類是所有 編碼 錯誤的基類。當一個該類型或者其子 類型 例如 ArgumentError 對象被拋出了,這意味著是 你代碼中的一個 bug。當你的 API 想要告訴調用者使用錯誤的時候 可以拋出一個 Error 來表明你的意圖。

同樣的,如果一個異常表示為運行時異常而不是代碼 bug, 則拋出 Error 則會誤導調用者。 應該拋出核心定義的 Exception 類 或者其他類型。

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

本條銜接上一天內容。既然 Error 表示代碼中的 bug,則需要修復 該問題而不是 捕獲該問題。

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

使用 rethrow 來重新拋出捕獲的異常。

如果你想重新拋出一個異常,推薦使用 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<bool> doAsyncComputation() async {
  try {
    var result = await longRunningCalculation();
    return verifyResult(result.summary);
  } catch(e) {
    log.error(e);
    return false;
  }
}

這是錯誤的示范:

Future<bool> doAsyncComputation() {
  return longRunningCalculation().then((result) {
    return verifyResult(result.summary);
  }).catchError((e) {
    log.error(e);
    return new Future.value(false);
  });
}

不要 在沒有有用效果的情況下使用 async 。

當成為習慣之后,你可能會在所有和異步相關的 函數使用 async。但是在有些情況下, 如果可以忽略 async 而不改變方法的行為,則應該這么做:

Future afterTwoThings(Future first, second) {
  return Future.wait([first, second]);
}

這是錯誤的示范:

Future afterTwoThings(Future first, second) async {
  return Future.wait([first, second]);
}

下面這些情況 async 是有用的:

  • 你使用了 await。 (這是一個很明顯的例子。)
  • 你在異步的拋出一個異常。 async 然后 throwreturn new Future.error(...) 要簡短很多。
  • 你在返回一個值,但是你希望他顯式的使用 Futureasyncnew Future.value(...) 要簡短很多。
  • 你不希望在事件循環發生事件之前執行 任何代碼。
Future usesAwait(Future later) async {
  print(await later);
}

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

Future asyncValue() async {
  return '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 = new Completer<bool>();

  new 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 new File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
Future<bool> fileContainsBear(String path) async {
  var contents = await new File(path).readAsString();
  return contents.contains('bear');
}

所屬標簽

無標簽

官方入門指南

Flutter官方發布的入門指導,包括了如何在不同的平臺(Windows, Mac, Linux)上搭建開發環境,以及一些入門級的指導,以便您從零開始進入Flutter的世界,同時,一些Flutter的框架API,也是您開發時必不可少的工具書。

從這里進入


25选5玩法中奖