Effective Dart 學習筆記

Effective Dart

閱讀 Effective Dart 時做的一些筆記。

Style Guide

好代碼一定是遵循良好代碼風格的代碼。前后一致的命名、排序和格式化使得代碼具有更高的可讀性。在整個 Dart 生態(tài)中維持一個規(guī)范統(tǒng)一的代碼風格,可以使得程序員之間分享彼此的代碼時,更容易閱讀和互相學習。

Identifiers

Dart 中標識符有三種類型:

  • UpperCamelCase:大駝峰命名法,每個單詞首字母大寫。
  • lowerCamelCase:小駝峰命名法,除了首個單詞,其余每個單詞首字母大寫。
  • lowercase_with_underscores:帶下劃線的小寫命名法,每個單詞小寫,用下劃線分割單詞。

DO name types using UpperCamelCase.

類名,枚舉類,typedef,注解等的命名都應該采用大駝峰命名法。

class SliderMenu { ... }

enum Status { ... }

typedef Predicate<T> = bool Function(T value);

@Foo(anArg)
class A { ... }

#### DO name extensions using `UpperCamelCase`.

和類型命名一樣,擴展方法也應該使用大駝峰命名法。

```dart
extension MyFancyList<T> on List<T> { ... }

DO name libraries, packages, directories, and source files using lowercase_with_underscores.

庫名,包名,文件和文件夾名,都應該使用小寫+下劃分隔符命名。

library my_library;

export 'global_constants.dart';
export 'src/common_util/screen_util.dart';

DO name import prefixes using lowercase_with_underscores.

導入包名使用別名時,用小寫+下劃分隔符。

import 'package:angular_components/angular_components' as angular_components;

DO name other identifiers using lowerCamelCase.

頂層方法,變量,類的成員,變量,參數(shù)等,都應該使用小駝峰命名法。

var httpRequest;

void alignItems(bool clearItems) {
  // ...
}

PREFER using lowerCamelCase for constant names.

推薦使用小駝峰命名常量、枚舉類。

const defaultTimeout = 1000;
final urlScheme = RegExp('^([a-z]+):');

class Dice {
  static final numberGenerator = Random();
}

DO capitalize acronyms and abbreviations longer than two letters like words.

大于兩個字符的縮寫均當做普通單詞使用。

class HttpConnection {} // bad: HTTPConnection
class DBIOPort {} // bad: DbIoPort
class TVVcr {}
class MrRogers {}

var httpRequest = ...
var uiHandler = ...
Id id;

PREFER using _, __, etc. for unused callback parameters.

推薦使用 _ 命名未使用的回調參數(shù)。

futureOfVoid.then((_, __, ___) {
  print('Operation complete.');
});

DON’T use a leading underscore for identifiers that aren’t private.

不要使用下劃線作為標識符的起始字符,只有私有變量和私有函數(shù)才可以使用下劃線開頭。

var _notVisible;

_initContentForCaseA() {
  ...
}

DON’T use prefix letters.

不要使用前綴字符。

var defaultTimeout; // bad: kDefaultTimeout

Ordering

為了保持一個 Dart 文件的整潔性,我們應該保證每段代碼都按照特定的順序排列,并且每個區(qū)域都被空白行分割。

DO place “dart:” imports before other imports.

import 'dart:async';
import 'dart:html';

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

DO place “package:” imports before relative imports.

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'util.dart';

DO specify exports in a separate section after all imports.

import 'src/error.dart';
import 'src/foo_bar.dart';

export 'src/error.dart';

DO sort sections alphabetically.

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
import 'package:gold/gold.dart';

Formatting

為了使得代碼具有良好的可讀性,我們需要格式化 Dart 代碼。

DO format your code using dart format.

Dart 為我們提供了 dart format 工具格式化代碼,具體見相關文檔

CONSIDER changing your code to make it more formatter-friendly.

在某些場景下,格式化可能會失效,比如很長的標識符、嵌套很深的表達式、多種操作符的混合使用等,我們應該考慮修改代碼使得代碼更加容易被格式化。

AVOID lines longer than 80 characters.

單行代碼越長越難讀,考慮換行,或者將一些特別長的標識符簡寫。

DO use curly braces for all flow control statements.

哪怕只有流程語句中只有一個表達式也應該使用花括號。

Documentation Guide

良好的文檔和注釋可以使得閱讀你的代碼的人(包括未來的你)更容易理解你的代碼。簡潔、精確的注釋可以節(jié)約一個人大量的時間和精力去理解看懂一段代碼所需要的上下文。雖然好的代碼是自解釋的,但是大多數(shù)時候我們都應該寫更多的注釋而不是更少的注釋。

Comment

DO format comments like sentences.

應該像寫普通的句子一樣寫注釋,注釋盡量使用英文,首字符前加空格,首字母大寫,每一句都應該使用標點。如果使用中文,則應該在中英文之間使用空格分割。

// Not if there is nothing before it.
if (_chunks.isEmpty) return false;
// 盡量減少使用中文注釋,即使使用也要 use space 分割中英文字符。
if (_chunks.specialCases) return true;

DON’T use block comments for documentation.

Dart 中不推薦使用塊注釋。

greet(name) {
  // Assume we have a valid name.
  print('Hi, $name!');
  // bad:
  /* Assume we have a valid name. */
  print('Hi, $name!');
}

Doc comments

文檔注釋 /// 可以用于方便地生成文檔頁,主要通過 dartdoc 生成。

DO use /// doc comments to document members and types.

對于類和成員變量變量,使用 /// 注釋,這樣 dartdoc 才能找到它們并解析成文檔。

/// The number of characters in this chunk when unsplit.
int get length => ...

PREFER writing doc comments for public APIs.

不用給每個類,成員變量寫注釋,但是至少關鍵部分應該寫文檔注釋。

CONSIDER writing a library-level doc comment.

最好在 Dart library 的入口提供文檔注釋,比如 library 的主要功能、術語解釋、樣本代碼、常用類和方法的鏈接、外部引用資源等。

CONSIDER writing doc comments for private APIs.

一些重要的私有方法也應該提供文檔注釋,方便庫的使用者理解你的代碼。

DO start doc comments with a single-sentence summary.

每個文檔注釋的開頭應該使用簡單、準確的一句話作為總結概括。

DO separate the first sentence of a doc comment into its own paragraph.

第一句總結之后使用空白行分割,可以使得文檔注釋更易讀。

AVOID redundancy with the surrounding context.

為類的成員和方法寫注釋時,避免寫得太過啰嗦,因為讀者對類的基本用途和上下文已經有了解了。

PREFER starting function or method comments with third-person verbs.

使用第三人稱動詞作為方法注釋的開頭,因為方法一般都是執(zhí)行一項任務,這樣可以讓讀者更快了解方法的用途。

PREFER starting variable, getter, or setter comments with noun phrases.

使用名詞作為變量注釋的開頭,因為變量一般代表一條數(shù)據(jù)。

PREFER starting library or type comments with noun phrases.

使用名詞作為庫和類型注釋的開頭,同樣庫和類型是一種對象火種功能的抽象。

CONSIDER including code samples in doc comments.

復雜的代碼中如果包含示例代碼有助于讀者理解你的代碼。

DO use square brackets in doc comments to refer to in-scope identifiers.

文檔注釋中使用中括號引用當前上下文中代碼的鏈接。

/// Throws a [StateError] if ...
/// similar to [anotherMethod()], but ...
/// Similar to [Duration.inDays], but handles fractional days.
/// To create a point from polar coordinates, use [Point.polar()].

DO use prose to explain parameters, return values, and exceptions.

Java 中一般使用 @param, @returns, @throws 等注解來注釋參數(shù)和返回值等,在 Dart 中不要這么做,推薦使用直白文字的敘述方法的功能,參數(shù)以及特殊的地方。

DO put doc comments before metadata annotations.

文檔注釋應該在注解之前。

/// A button that can be flipped on and off.
@Component(selector: 'toggle')
class ToggleComponent {}

Markdown

Dart 中支持大多數(shù)常見的 markdown 語法。

AVOID using markdown excessively.

When in doubt, format less. Formatting exists to illuminate your content, not replace it. Words are what matter. (說得太經典了,拿小本本記下來(??????))

AVOID using HTML for formatting.

大多數(shù)情況下,應該只使用 markdown 語法就夠了。

PREFER backtick fences for code blocks.

我們可以使用 inline code 或者 ``` code blocks,如果是小段的代碼可以使用前者,如果是大段的代碼塊,推薦使用后者。

Writing

作為程序員,雖然我們每天主要和計算機打交道,但是我們寫得絕大多數(shù)代碼都是給人讀的,寫代碼需要練習,寫作也同樣需要練習。

這里介紹一些寫作技巧,更多關于技術寫作的技巧,推薦閱讀:Technical writing style.

PREFER brevity.

保證你的文字是清晰和精準的,同時保持簡潔。

AVOID abbreviations and acronyms unless they are obvious.

盡可能少使用縮寫,除非是那種人人都知道的,比如 "i.e.", "e.g.", "etc"。

PREFER using “this” instead of “the” to refer to a member’s instance.

當需要代指當前類的實例是時候,使用 this 代替 the。

class Box {
  /// True if this box contains a value.
  bool get hasValue => _value != null;
}

Usage Guide

Libraries

DO use strings in part of directives.

在 Dart 中,我們可以使用 part of 將代碼分離到另一個文件中,然后使用 part 引用這部分分離出去的代碼,推薦的做法是,直接使用 URI 鏈接到指定的文件,而不是只指定庫的名字。

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

// bad:
part of my_library;

DON’T import libraries that are inside the src directory of another package.

我們應該直接導入庫,而不是導入庫中的某個文件或者目錄,因為庫作者可能對目錄結構做修改。

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

比如使用相對路徑引用 lib 之外的某個文件,或者 lib 之外的某個文件(比如 test 文件夾)導入 lib 內的文件,這兩種情況都應該避免,應該只使用包導入的方式導入依賴。

import 'package:my_package/api.dart';

// bad:
import '../lib/api.dart';

PREFER relative import paths.

如果無法使用包導入,才使用相對路徑的方式導入。

test/api_test.dart:

import 'test_utils.dart'; // 在當前同一個目錄結構下,可以使用相對路徑

Null

DON’T explicitly initialize variables to null.

Dart 會自動為可為空的變量賦值為 null,而不可為空的變量在初始化之前被使用的話會報錯。所以沒必要為變量賦空值。

DON’T use an explicit default value of null.

與上一條類似,如果你為一個可選位置參數(shù)的值可為空,那么 Dart 會為它自動賦值為空值,沒必要手動賦值為空。

PREFER using ?? to convert null to a boolean value.

使用 ?? 將空值轉換為布爾值,好處更簡潔易懂??蠢樱?/p>

// If you want null to be false:
if (optionalThing?.isEnabled ?? false) {
  print("Have enabled thing.");
}

// If you want null to be true:
if (optionalThing?.isEnabled ?? true) {
  print("Have enabled thing or nothing.");
}

// 第二種情況等同于下面這種寫法,所以使用 ?? 明顯可以簡化寫法
if (optionalThing?.isEnabled == null || optionalThing!.isEnabled!) {
    print("Have enabled thing or nothing.");
}

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

使用 late 關鍵字可以讓我們延遲初始化某些變量,但是這樣我們就沒法判斷某個變量是否初始化了,當需要做這樣的判斷時候,最好的做法是不要使用 late 關鍵字,而是使用 nullable 變量。

// 使用 late 關鍵字
late Type a;
bool initialized = false;

initialize() {
  a = Type('A');
  initialized = true;
}

doSomeWorkEnsureInitialized() {
  if (a == null) {
    if (!initialized) {
      intialize();
    }
  }
  doWork();
}

// 不使用 late 關鍵字
Type? a;

doSomeWorkEnsureInitialized() {
  if (a == null) {
    initialize();
  }
  doWork();
}

CONSIDER copying a nullable field to a local variable to enable type promotion.

Dart 中有類型提升的機制,但是只適用于本地變量,因此,對于可為空的成員變量我們應該先將其拷貝成本地變量,然后再使用。

final Response? response;

@override
String toString() {
  var 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).";
}

但是要注意如果本地變量發(fā)生變化,要將其重新賦值到成員變量上。而且成員變量有可能在被拷貝之后發(fā)生了變化,則拷貝的本地變量已經「過時」了。

Strings

DO use adjacent strings to concatenate string literals.

Dart 中不需要使用 + 來連接兩個字符,像 C 和 C++ 中一樣,相鄰的字符串會自動被拼接成同一個字符串。

print('content1,''content2,');
print('Some very very long string text which cannot be put into one line, '
      'but can be put into adjacent line to be concatenated together without +');

PREFER using interpolation to compose strings and values.

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

Collections

DO use collection literals when possible.

創(chuàng)建集合的時候使用字面量表達式,而不是默認構造器,因為字面量表達式還支持擴展表達式和集合 if 和集合 for 的操作。

// good:
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};

// bad:
var points = List<Point>.empty(growable: true);
var addresses = Map<String, Address>();
var counts = Set<int>();

DON’T use .length to see if a collection is empty.

使用 isEmptyisNotEmpty 代替 .length。

if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');

AVOID using Iterable.forEach() with a function literal.

使用 for..in 代替 forEach()。

// good:
for (var person in people) {
  ...
}

// bad:
people.forEach((person) {
  ...
});

DON’T use List.from() unless you intend to change the type of the result.

使用 iterable.toList() 代替 List.from(iterable)。

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

只有在需要修改集合類型的時候才使用 List.from()。

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

DO use whereType() to filter a collection by type.

使用 whereType() 根據(jù)類型篩選集合。

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

DON’T use cast() when a nearby operation will do.

集合方法很多都支持泛型,所以除非必要,不要使用 cast() 進行類型轉換。

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

var reciprocals = stuff.map<double>((n) => 1 / n);
// var reciprocals = stuff.map((n) => 1 / n).cast<double>();

AVOID using cast().

如非必要,盡量減少使用 cast() 轉換集合類型,考慮使用以下方法代替:

  • 直接用目標類型創(chuàng)建集合。

  • 遍歷元素,對單個元素使用 cast()(使用時才轉換)。

  • 盡量使用 List.from() 代替 cast()。

cast() 方法會返回一個集合并且在每次操作時檢查元素類型,如果你只在集合的少量元素上做操作則適合使用 cast() 方法,其它情況下,這種轉換的性能開銷都比較大。

Functions

DO use a function declaration to bind a function to a name.

當使用局部方法的時候,如果需要有方法名,盡量直接使用方法聲明,避免使用 lambd 表達式+變量給方法命名。

void main() {
  // good:
  localFunction() {
    ...
  }
  
  // bad:
  var localFunction = () {
    ...
  };
}

DON’T create a lambda when a tear-off will do.

當使用匿名函數(shù)的時候,如果函數(shù)調用的方法所需的參數(shù)和函數(shù)的參數(shù)一致,則可以使用 tear-off 的語法,類似于 Java 中的方法引用。

// good:
names.forEach(print);

// bad:
names.forEach((name) {
  print(name);
});

DO use = to separate a named parameter from its default value.

由于歷史遺留問題,Dart 中允許使用 : 為參數(shù)設置默認值,最好的做法是用 =。

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

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

Variables

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

絕大多數(shù)局部變量都不需要有類型標注,而是直接使用 var 或者 final 關鍵字聲明。我們可以選擇以下兩個原則的其中一個,不要同時使用兩種方式:

  • 原則 A:如果是不會被重新賦值的變量,則使用 final 關鍵字,會被重新賦值的則使用 var 關鍵字。
  • 原則 B:所有的局部變量一律都使用 var 關鍵字,只有頂層變量和成員變量才使用 final 關鍵字。

推薦原則 B,更簡單,且容易遵循。

AVOID storing what you can calculate.

原因是浪費內存,推薦使用 getters 代替。

class Circle {
  double radius;

  Circle(this.radius);

  // 不要使用成員變量來保存需要計算得到的值,使用 getters
  double get area => pi * radius * radius;
  double get circumference => pi * 2.0 * radius;
}

Members

Dart 中,對象的成員可以是方法或者變量。

DON’T wrap a field in a getter and setter unnecessarily.

Dart 中訪問屬性和訪問 getters/setters 的效果是一樣的,每個屬性默認會自動生成 getters/setters,所以沒必要手動去寫 getters/setters,如果是為了使屬性不可訪問,則應該使用私有屬性。

PREFER using a final field to make a read-only property.

// good:
class Box {
  final contents = [];
}

// bad:
class Box {
  var _contents;
  get contents => _contents;
}

CONSIDER using => for simple members.

使用箭頭表達式定義成員變量,最常見的用法是用于創(chuàng)建 getters。

double get right => left + width;
set right(double value) => left = value - width;

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

DON’T use this. except to redirect to a named constructor or to avoid shadowing.

只有以下幾種情況下才使用 this 關鍵字:

  • 構造器中引用成員變量;
  • 構造器重定向到某個具名構造器時;
  • 成員變量與局部變量或者參數(shù)同名時;

有趣的是,在構造器初始化列表中,可以區(qū)分出同名參數(shù),所以不需要使用 this

class Box extends BaseBox {
  var value;

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

DO initialize fields at their declaration when possible.

如果成員屬性不依賴構造器初始化,則應該盡量在聲明處進行初始化。

class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

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

Constructors

DO use initializing formals when possible.

在構造器參數(shù)前使用 this. 叫做 initializing formal

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

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

如果成員變量會在構造器中進行初始化,就應該避免將其標記為 late

DO use ; instead of {} for empty constructor bodies.

構造器方法體為空時使用 ; 結束構造器,避免使用空方法體。

DON’T use new.

避免使用 new 關鍵字,同樣是 Dart 的歷史遺留問題。

DON’T use const redundantly.

以下幾種情況下,不需要使用 const 關鍵字:

  • 在一個 const 的集合中;
  • 在一個 const 構造器中,其中每個參數(shù)都是 const 的;
  • 在元數(shù)據(jù)注解中;
  • const 常量的初始化器;
  • 在 switch..case 表達式中,case 之后 : 之前的值默認也是 const 的;

Error handling

AVOID catches without on clauses.

如果 catch 子句沒有使用 on 關鍵字限定捕捉的異常類型,則會捕捉所有類型的異常,這樣我們就沒法得到程序出錯的具體原因了,到底是 stackoverflow 還是參數(shù)異常,又或者是斷言條件未滿足?所以,哪怕是只捕捉 Exception 也比捕捉全部異常好,Exception 代表程序運行時異常,排除了編碼錯誤造成的異常。

DON’T discard errors from catches without on clauses.

如果你執(zhí)意捕捉所有異常,請不要丟棄異常內容,至少打印一下好嗎?

DO throw objects that implement Error only for programmatic errors.

所有的編碼異常都繼承自 Error 類,如果是運行時異常造成的錯誤,則應該拋出運行時異常,盡量在編碼異常中排除掉運行時異常。

DON’T explicitly catch Error or types that implement it.

實現(xiàn)了 Error 類的大多是編碼異常,這類異??梢愿嬖V我們程序出錯的信息,通常不應該捕捉這類異常。

DO use rethrow to rethrow a caught exception.

throw 會重置錯誤棧信息,而 rethrow 則不會。

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

Asynchrony

PREFER async/await over using raw futures.

async / await 語法可以提升代碼的可讀性,并且是的程序更容易 debug。

DON’T use async when it has no useful effect.

雖然異步場景下優(yōu)先推薦使用 async,但是注意不要濫用 async。滿足以下條件時才推薦使用 async

  • 使用了 await.(廢話)
  • 需要異步返回異常,使用 async 比使用 Future.error() 更簡潔。
  • 需要返回一個 Future 對象,使用 async 比使用 Future.value() 更簡潔。

CONSIDER using higher-order methods to transform a stream.

Stream 和集合類似,包含一些諸如 every, any, distinct 等便捷的轉換操作,避免手動轉換。

AVOID using Completer directly.

Completer 是比較底層的類,應該避免使用,大多數(shù)場景下用 async/await 就足夠了。

DO test for Future<T> when disambiguating a FutureOr<T> whose type argument could be Object.

在使用 FutureOr<T> 之前,你通常需要先用 is 關鍵字判斷其類型。因為 T 有可能是 Object,而 FutureOr<> 本身也是 Object,所以要用 Future<T> 作為判斷依據(jù)。

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

Design Guide

Names

DO use terms consistently.

在整個代碼庫中保持用相同的名稱命名相同的物體,盡量使用被大眾接受或者共同認可的方式命名。

pageCount         // A field.
updatePageCount() // Consistent with pageCount.
toSomething()     // Consistent with Iterable's toList().
asSomething()     // Consistent with List's asMap().
Point             // A familiar concept.

AVOID abbreviations.

盡量避免使用縮寫,除非是非常常見的縮寫。

PREFER putting the most descriptive noun last.

最后一個單詞一定是最具描述性、最能總結該類用途的名詞。

StatelessWidget
DownloadPage
AnimationController
OutlineInputBorder

CONSIDER making the code read like a sentence.

盡量使你的代碼讀起來可以像讀文章一樣易懂。

if (serviceTable.isEmpty) {
  serviceTable.addAll(waitingList.where(
          (customer) => customer.waitingMinutes > 30));
}

PREFER a noun phrase for a non-boolean property or variable.

盡量使用名詞命名非布爾類型的屬性或變量。

PREFER a non-imperative verb phrase for a boolean property or variable.

使用非祈使動詞命名布爾類型的屬性或變量。布爾類型的變量一般代表某種狀態(tài)或者能力。

isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup

CONSIDER omitting the verb for a named boolean parameter.

具名參數(shù)一般可以省略動詞,使用形容詞。

var copy = List.from(elements, growable: true);
var copy = List.from(elements, canGrow: true); // 哪一種更好?

PREFER the “positive” name for a boolean property or variable.

對于布爾值,盡量使用有價值、有意義、有用的值作為 true 值等。

if (socket.isConnected && database.hasData) {
  socket.write(database.read());
}

PREFER an imperative verb phrase for a function or method whose main purpose is a side effect.

使用祈使動詞短語命名那些具有 "side effect" 的方法名,side effect 即會改變對象的內部狀態(tài),比如屬性等,或者產生一些新數(shù)據(jù),或者會引起外部其它對象發(fā)生變化等等。

queue.removeFirst();
window.refresh();

PREFER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.

如果方法的主要用途是返回值,那么應該使用名詞短語或者非祈使動詞短語作為方法名。

var element = list.elementAt(3);
var first = list.firstWhere(condition);
var char = string.codeUnitAt(4);

CONSIDER an imperative verb phrase for a function or method if you want to draw attention to the work it performs.

當方法不產生任何 side effect 但是會比較消耗資源或者做一些有可能出錯的操作時,也要使用祈使動詞短語命名。

var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();

AVOID starting a method name with get.

大多數(shù)方法都應該直接省略 get 直接描述方法作用,比如使用 packageSortOrder() 而不是 getPackageSortOrder()。

PREFER naming a method toXxx() if it copies the object’s state to a new object.

如果方法的主要用途是將一個對象復制并轉換為另一個對象時,盡量使用 toXxx() 命名。

list.toSet();
stackTrace.toString();
dateTime.toLocal();

PREFER naming a method asXxx() if it returns a different representation backed by the original object.

如果方法的主要用途是基于一個對象包裝轉換成另一個對象時,盡量使用 asXxx() 命名。

var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();

AVOID describing the parameters in the function’s or method’s name.

方法名中不要帶有參數(shù)相關的信息。

// good:
list.add(element);
map.remove(key);

// bad:
list.addElement(element);
map.removeKey(key);

只有在為了區(qū)分多種功能類似的方法時才可以忽略該原則:

map.containsKey(key);
map.containsValue(value);

DO follow existing mnemonic conventions when naming type parameters.

使用現(xiàn)有的助記規(guī)則命名類型參數(shù),比如 E 代表集合中的 Element,K/V 代表 Map 的 key/value,R 代表類或方法的 return 值,其它情況使用 T/S/U/N/E 等。

Libraries

Dart 中使用下劃線 __ 代表成員是庫私有的,這不僅僅是約定俗成的,更是在語法層面做出的規(guī)定。

PREFER making declarations private.

對于庫的作者而言,這點尤為重要,要盡可能減少暴露接口給庫的使用者,只暴露必須使用到的接口。

CONSIDER declaring multiple classes in the same library.

Dart 中每個文件都是一個 library,但是不像 Java 等語言一個文件通常只能代表一個類,Dart 中可以在相同的 library 中包含多個類、頂層變量和方法,只要這些類、變量和方法的確相互聯(lián)系并且構成一個功能模塊就可以了。

將多個類放在一起有諸多好處。因為私有訪問權限僅限于庫層級,而不是類層級,所以在同一個庫中是可以訪問其它類中的私有屬性和方法的。

Classes and mixins

Dart 是一個純面向對象的語言,這意味這所有對象都是類的實例。但是另一方面,Dart 有可以像面向過程或者函數(shù)式編程一樣擁有頂層方法和變量。

AVOID defining a one-member abstract class when a simple function will do.

當類中只有一個方法或者變量的時候,考慮使用頂層變量或者方法代替。

// good:
typedef Predicate<E> = bool Function(E element);

// bad:
abstract class Predicate<E> {
  bool test(E element);
}

AVOID defining a class that contains only static members.

Java C# 等語言必須將方法、變量和常量定義在類之中,比如 Java 中,我們常常會用一個 Constants 類來保存全局使用到的一些靜態(tài)常量。而且為了避免命名沖突,我們常常會使用類名區(qū)分相同名字的方法。Dart 中沒有這樣的限制,相反,Dart 使用 library 作為命名空間,并且在導入包的時候可以使用 as/show/hide 等關鍵字避免沖突的命名。

我們可以將靜態(tài)方法或靜態(tài)成員變量轉換成頂層方法或常量,然后以合適的方式命名或者整理進同一個庫中。當然,這個規(guī)則不一定必須要遵循,一些場合下可能還是使用類和靜態(tài)成員變量更合適:

class Color {
  static const red = '#f00';
  static const green = '#0f0';
  static const blue = '#00f';
  static const black = '#000';
  static const white = '#fff';
}

AVOID extending a class that isn’t intended to be subclassed.

有些類可能本意就不是為了被繼承而設計的,所以盡量用合適的命名告訴庫的使用者哪些類可以被繼承而哪些類不行。

DO document if your class supports being extended.

同上,如果一個類可以被繼承,至少用注釋說明一些需要注意的地方。

AVOID implementing a class that isn’t intended to be an interface.

同樣的,如果一個接口不應該被實現(xiàn),而使用者卻實現(xiàn)了這個接口,那么當未來庫的作者對這個接口做任何改動,都會影響到使用者的原有的實現(xiàn)。

DO document if your class supports being used as an interface.

如果一個接口可以被實現(xiàn),需要在文檔注釋中說明。

DO use mixin to define a mixin type.

Dart 從版本 2.1.0 之后才添加了 mixin 關鍵字,在這之前,任何沒有默認構造器、沒有父類的類都可以被用做 mixin,這帶來了一個問題,有些類可能并不適合用做 mixin,誤用它們可能會帶來一些問題。而使用了 mixin 關鍵字之后,mixin 類只能被用做 mixins,而不能用做其它用途。

AVOID mixing in a type that isn’t intended to be a mixin.

同上。

Constructors

Dart 中構造器是一類特殊的方法,用于創(chuàng)建類的實例,它的方法名和類名相同,沒有返回值,并且除此之外還可以用 . 分割,后面加標識符,這類構造器叫具名構造器。

CONSIDER making your constructor const if the class supports it.

如果類中的成員變量都是 final 的,而且構造器只是初始化了這些成員變量,那么可以用 const 修飾構造器。這樣,使用該類的開發(fā)者就可以在需要使用常量的地方使用該類的對象了,比如在其它常量容器中,switch..case 中,默認參數(shù)值等等。

class Pet {
  final String name;
  
  const Pet(this.name);
}

class PetShop {
  Pet pet;

  // Error: The default value of an optional parameter must be constant.
  // PetShop({this.pet = Pet('Cat')}); 
  
  PetShop([this.pet = const Pet('Cat')]);
}

Members

PREFER making fields and top-level variables final.

盡量將成員變量和頂層變量設為 final 的。

DO use getters for operations that conceptually access properties.

在 API 設計中,使用 getters 還是使用方法作為訪問某個數(shù)據(jù)的方式是個微妙但重要的部分。Dart 中成員變量會自動生成 getters,所以訪問成員變量和訪問 getters,其本質是一樣的。所以,通常來說,訪問 getters 給人的感覺就像是訪問成員變量,它意味著:

  • 這種操作不需要接受參數(shù),但是有返回值;
  • 調用者只關心返回值;
  • 這種操作并不會造成任何用戶可見的 side effects;
  • 這種操作具有冪等性,即無論調用多少次,結果相同;
  • 返回的結果對象不會暴露原始對象的所有狀態(tài);

如果你的目標操作符合以上所有特點,則可以將這種操作定義成一個 getter 而不是方法。

DO use setters for operations that conceptually change properties.

與 getters 類似,使用 setters 也會遇到類似的困境,不同的是 setters 需要滿足 filed-like 特質:

  • 操作只接受一個參數(shù),且不會產生返回值;
  • 操作只會改變對象的某些狀態(tài);
  • 操作具有等冪性;

DON’T define a setter without a corresponding getter.

一個可被修改的 setter 往往對應著一個供訪問的 getter。

AVOID using runtime type tests to fake overloading.

Dart 中沒有重載機制,有的人會定義一個方法,在方法中用 is 判斷類型然后根據(jù)具體的類型做一些操作。這種操作雖然能達到目的,但是最好的做法還是使用一系列獨立的方法,讓用戶根據(jù)不同的類型調用不同的方法。只有當一個對象具體類型不確定,需要在運行時根據(jù)不同的類型來調用特定的子類方法的時候,才可以把它們定義在一個方法內。

AVOID public late final fields without initializers.

不像其它 final 成員變量,late final 的成員變量如果沒有初始化器的話,Dart 會為它們生成 setters 函數(shù),如果成員變量是 public 的,則意味著 setters 也是 public 的。我們將成員變量設置為 late 通常是希望稍后再去初始化它,比如在構造器中,而 late final 使得成員變量可能在被初始化之前就在外部被初始化了一遍。所以,最好的做法是:

  • 不要使用 late;
  • 使用 late 但是在聲明時為其初始化;
  • 使用 late 但是將它標記為 private 的,同時為其提供一個 getter;

AVOID returning nullable Future, Stream, and collection types.

如果返回的是容器類型,盡量避免返回空的容器類型,一般使用返回空的容器或者直接返回 null。

AVOID returning this from methods just to enable a fluent interface.

使用級聯(lián)表達式而不是在方法中返回 this 實現(xiàn)鏈式調用。

Types

我們使用類型標注限制某部分代碼能夠使用什么樣的值。類型通常出現(xiàn)在兩個地方:變量聲明處的類型標注 (type annotations) 和使用泛型時的類型參數(shù) (generic invocations)。

類型標注通常就是我們所認為的靜態(tài)類型,我們可以對變量、參數(shù)、屬性、返回值使用類型標注,就像下面這個例子:

bool isEmpty(String parameter) { // 返回值和參數(shù)的類型標注
  bool result = parameter.isEmpty; // 變量的類型標注
  return result;
}

泛型調用時的類型參數(shù)可以是創(chuàng)建集合字面量,或者是調用泛型類的構造方法,或者是調用泛型方法。比如:

var lists = <num>[1, 2]; // 創(chuàng)建集合時指定類型
lists.addAll(List<num>.filled(3, 4)); // 調用泛型類的構造器并指定類型
lists.cast<int>(); // 調用泛型方法

Type inference

類型標注是可選的,因為 Dart 會根據(jù)當前上下文推斷出具體類型。當缺乏足夠的信息推斷出類型的時候,Dart 默認會使用 dynamic 類型。這種機制讓類型推斷看起來是安全的,但實際上使得類型檢查完全失效了。

同時擁有類型推斷和 dynamic 類型,使得我們在說代碼是無類型的 (untyped) 時產生歧義,一個變量到底是動態(tài)類型還是沒有寫類型參數(shù)?所以,我們一般不說代碼是無類型的,而是使用以下術語代替:

  • 如果代碼擁有類型標注,則它的類型即所標注的類型(廢話)。
  • 如果代碼是推斷類型,則說明 Dart 已經確定其類型。而如果類型推斷失敗,那么我們不把稱它為 inferred
  • 如果代碼是動態(tài)類型的,那么它的靜態(tài)類型就是 dynamic。這種情況下,代碼既可以是主動被標注為 dynamic 也可以是推斷類型(使用 var 關鍵字)。

換句話說,代碼是標注類型還是推斷類型,與它是否被標注為 dynamic 或者其它類型無關。

類型推斷是個強有力的工具,可以幫助我們編碼或閱讀代碼時跳過一些顯而易見的部分(代碼類型),讓我們關注真正重要的代碼邏輯。但是,顯式的代碼類型也同樣重要,它可以幫助我們寫出健壯、可維護的代碼。

當然,類型推斷也不是萬能藥,一些情況下還是應該使用類型標注。有時候類型推斷提前確定了變量類型,但是該類型不是你想要的,比如變量在初始化后推斷出了類型,但是你實際卻想要使用另一個類型,這種情況下就只能使用顯式的類型標注了。

理解上面這些概念之后,方便我們在解釋接下來的這些原則時,不會造成歧義。首先,我們可以將大致的原則總結為以下幾點:

  • 當上下文不足以推斷出類型的時候,請使用類型標注,即使你想要的是 dynamic 類型;
  • 不要標注局部變量或者泛型調用;
  • 對于頂層變量和屬性,盡量顯式標注其類型,除非初始化器使得它們的類型顯而易見;

DO type annotate variables without initializers.

如果沒有變量沒有立即被初始化,請使用類型標注。

DO type annotate fields and top-level variables if the type isn’t obvious.

如果變量類型不是顯而易見的,也要使用類型標注。

顯而易見包括以下這些情況:

  • 字面量,如基本數(shù)據(jù)類型等
  • 構造器中的參數(shù)
  • 引用其它變量或者常量
  • 簡單的表達式,比如 isEmpty, ==, > 等等
  • 工廠方法,比如 int.parse(), Future.wait() 等

另外,當你覺得類型標注可以使你的代碼更清晰時,那就請使用類型標注。

When in doubt, add a type annotation.

DON’T redundantly type annotate initialized local variables.

有初始化器的局部變量不要使用類型標注。只有當你確定推斷類型不是你想要的類型的時候才使用類型標注。

DO annotate return types on function declarations.

給方法返回值添加類型標注可以方便方法的調用者。當然,匿名方法就沒必要了。

DO annotate parameter types on function declarations.

給方法的參數(shù)添加類型標注,同樣很有必要,可以幫助方法的調用者確定參數(shù)的邊界。

需要注意的是,Dart 不會對可選的參數(shù)做類型推斷,來源

void sayRepeatedly(String message, {int count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}

DON’T annotate inferred parameter types on function expressions.

Dart 通??梢愿鶕?jù)上下文確定匿名方法接收的參數(shù)是什么,所以匿名方法一般不需要添加類型標注。

var names = people.map((person) => person.name);

DON’T type annotate initializing formals.

之前說過構造器中使用 this. 給屬性賦值的形式叫做 initializing formals,這種情況下也不要使用類型標注。

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

DO write type arguments on generic invocations that aren’t inferred.

一些情況下,泛型的類型無法被確定,比如空的集合,所以我們需要為它們標注類型。

var playerScores = <String, int>{};
final events = StreamController<Event>();

// 對于成員變量來說,如果類型同樣無法推斷出,則需要在聲明處標注類型
class Downloader {
  final Completer<String> response = Completer();
}

DON’T write type arguments on generic invocations that are inferred.

如果泛型類的類型已經推斷出來,就不要在寫類型了。

class Downloader {
  final Completer<String> response = Completer<String>(); // 錯誤示例
}

AVOID writing incomplete generic types.

也就是不要使用 raw 泛型。

// bad:
List numbers = [1, 2, 3];
var completer = Completer<Map>();

// good:
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();

DO annotate with dynamic instead of letting inference fail.

顯式標明 dynamic 永遠要比不寫類型標注要好。

// good:
dynamic mergeJson(dynamic original, dynamic changes) => ...
// bad:
mergeJson(original, changes) => ...

當然,有些情況下,Dart 也能推斷出 dyanmic 類型的:

Map<String, dynamic> readJson() => ...

void printUsers() {
  var json = readJson();
  var users = json['users'];
}

PREFER signatures in function type annotations.

默認的 Function 允許任何類型的返回值和參數(shù),如果不帶簽名使用,在有些情況下會導致錯誤。

// good:
bool isValid(String value, bool Function(String) test) => ...
// bad:
bool isValid(String value, Function test) => ...

DON’T specify a return type for a setter.

Dart 中 setters 只會返回 void,所以不需要寫返回值。

DON’T use the legacy typedef syntax.

又是一個歷史遺留問題,Dart 中有兩種方式定義 typedef,推薦使用新的寫法。

// bad:
typedef int Comparison<T>(T a, T b);
// good:
typedef Comparison<T> = int Function(T a, T b);

PREFER inline function types over typedefs.

Dart 2 開始支持 inline function,我們可以直接定義方法作為類型簽名。

class FilteredObservable {
  final bool Function(Event) _predicate;
  final List<void Function(Event)> _observers;

  FilteredObservable(this._predicate, this._observers);

  void Function(Event)? notify(Event event) {
    if (!_predicate(event)) return null;

    void Function(Event)? last;
    for (var observer in _observers) {
      observer(event);
      last = observer;
    }

    return last;
  }
}

如果方法很復雜或者多次使用的情況下,推薦使用 typedef 代替。

PREFER using function type syntax for parameters.

就像方法可以作為類型標注一樣,方法也可以作為參數(shù),并且有特殊的語法支持:

// 函數(shù)形式的參數(shù):返回值 Function(參數(shù)類型) 參數(shù)名
Iterable<T> where(bool Function(T) predicate) => ...

AVOID using dynamic unless you want to disable static checking.

Dart 中 dynamic 是一個非常特殊的類型,它的作用和 Object? 類似,都允許任何對象,包括 null,但是 dynamic 還有額外的功能,那就是默認允許任何操作,包括對任何成員的訪問,無論這種訪問是否有效或者合法,Dart 不會在編譯期對其進行檢查,如果有異常只會在運行期才會被拋出。除非你確認想要這種效果,否則還是使用 Obejct? 或者 Object 代替 dynamic,然后用 is 對類型進行進行檢查和類型提升。

/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(Object arg) {
  if (arg is bool) return arg;
  if (arg is String) return arg.toLowerCase() == 'true';
  throw ArgumentError('Cannot convert $arg to a bool.');
}

DO use Future<void> as the return type of asynchronous members that do not produce values.

如果異步方法沒有值需要返回,請使用 Future<void> 作為返回值。這樣可以保證后續(xù)的操作,還有支持 await 等。

AVOID using FutureOr<T> as a return type.

如果方法接受 FutureOr<int> 作為參數(shù),那么它可以接收 int 或者 Future<int> 作為參數(shù),這樣可以方便調用者用 Future 包裝 int 后再調用你的方法。但是,如果你返回 FutureOr<T>,方法的調用者就需要檢查返回值到底是 int 還是 Future<int>。推薦的做法是直接返回 Future<T>,這樣調用者可以直接使用 await 獲取異步結果值。

Future<int> triple(FutureOr<int> value) async => (await value) * 3;

Parameters

AVOID positional boolean parameters.

可選布爾值不但容易讓調用著分不清參數(shù)的含義,而且容易出錯。

// bad:
new ListBox(false, true, true);
// good:
ListBox(scroll: true, showScrollbars: true);

AVOID optional positional parameters if the user may want to omit earlier parameters.

對于可選位置參數(shù),調用者可能省略中間或者后面部分,盡量把關鍵部分寫在前面,或者使用具名位置參數(shù)。

// 調用方可能省略一部分可選位置參數(shù),因此,最重要的寫在前面
String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int? end]);

// 使用具名位置參數(shù)就沒有這個煩惱了
Duration(
    {int days = 0,
    int hours = 0,
    int minutes = 0,
    int seconds = 0,
    int milliseconds = 0,
    int microseconds = 0});

AVOID mandatory parameters that accept a special “no argument” value.

不要強制用戶傳 null,使用可選參數(shù)代替。

// bad:
var rest = string.substring(start, null);
// good:
var rest = string.substring(start);

DO use inclusive start and exclusive end parameters to accept a range.

當方法接收的參數(shù)用數(shù)字下標表示范圍時,盡量采用前閉后開的習俗,包括開頭下標但是不包括結尾的下標。

[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'

Equality

DO override hashCode if you override ==.

這是約定俗成的。兩個對象相同則說明它們的哈希值一致,否則類似于 Map 等基于哈希值的集合就無法使用了。

DO make your == operator obey the mathematical rules of equality.

  • 自反性:a == a 永遠返回 true;
  • 對稱性:a == b 為 true 時 b == a 也必定為 true;
  • 傳遞性:a == bb == c 都為 true,則 a == c 也為 true;

AVOID defining custom equality for mutable classes.

如果是可變的對象,比如擁有可變屬性的對象,他們的哈希值會隨著屬性的變化而變化,但是大多數(shù)基于哈希的集合沒有考慮到這一點,因此,最好不要自定義可變對象的相等性。

DON’T make the parameter to == nullable.

Dart 語言中 null 只能等于 null,因此,使用 == 比較對象時,右邊的對象不能是 null。

class Person {
  final String name;

  // bad:
  bool operator ==(Object? other) =>
      other != null && other is Person && name == other.name;
  
  // good:
  bool operator ==(Object other) => other is Person && name == other.name;
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 基本數(shù)據(jù)類型 Number intint a = 1;int b = 0xDEFFFF; doubledouble...
    清無閱讀 438評論 0 0
  • 語法學習筆記。資料參考:官方文檔(英文)(要直接看英文文檔,中文文檔可能是機器翻譯的,很多地方語句不通順,埋坑無數(shù)...
    SingleDigit閱讀 185評論 0 0
  • 如何閱讀指南 DO 應始終遵循的準則 DON'T 不應該這么使用的準則 PREFER 應該遵循的準則,但是在某些情...
    _白羊閱讀 3,092評論 0 3
  • 彩排完,天已黑
    劉凱書法閱讀 4,273評論 1 3
  • 表情是什么,我認為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息。高興了當然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,780評論 2 7