Effective Dart 学习笔记

Effective Dart

阅读 Effective Dart 时做的一些笔记。

Style Guide

好代码一定是遵循良好代码风格的代码。前后一致的命名、排序和格式化使得代码具有更高的可读性。在整个 Dart 生态中维持一个规范统一的代码风格,可以使得程序员之间分享彼此的代码时,更容易阅读和互相学习。

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.

顶层方法,变量,类的成员,变量,参数等,都应该使用小驼峰命名法。

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.

推荐使用 _ 命名未使用的回调参数。

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

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

不要使用下划线作为标识符的起始字符,只有私有变量和私有函数才可以使用下划线开头。

var _notVisible;

_initContentForCaseA() {
  ...
}

DON’T use prefix letters.

不要使用前缀字符。

var defaultTimeout; // bad: kDefaultTimeout

Ordering

为了保持一个 Dart 文件的整洁性,我们应该保证每段代码都按照特定的顺序排列,并且每个区域都被空白行分割。

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

良好的文档和注释可以使得阅读你的代码的人(包括未来的你)更容易理解你的代码。简洁、精确的注释可以节约一个人大量的时间和精力去理解看懂一段代码所需要的上下文。虽然好的代码是自解释的,但是大多数时候我们都应该写更多的注释而不是更少的注释。

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.

使用第三人称动词作为方法注释的开头,因为方法一般都是执行一项任务,这样可以让读者更快了解方法的用途。

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

使用名词作为变量注释的开头,因为变量一般代表一条数据。

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 等注解来注释参数和返回值等,在 Dart 中不要这么做,推荐使用直白文字的叙述方法的功能,参数以及特殊的地方。

DO put doc comments before metadata annotations.

文档注释应该在注解之前。

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

Markdown

Dart 中支持大多数常见的 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.

大多数情况下,应该只使用 markdown 语法就够了。

PREFER backtick fences for code blocks.

我们可以使用 inline code 或者 ``` code blocks,如果是小段的代码可以使用前者,如果是大段的代码块,推荐使用后者。

Writing

作为程序员,虽然我们每天主要和计算机打交道,但是我们写得绝大多数代码都是给人读的,写代码需要练习,写作也同样需要练习。

这里介绍一些写作技巧,更多关于技术写作的技巧,推荐阅读: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.

与上一条类似,如果你为一个可选位置参数的值可为空,那么 Dart 会为它自动赋值为空值,没必要手动赋值为空。

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

使用 ?? 将空值转换为布尔值,好处更简洁易懂。看例子:

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

但是要注意如果本地变量发生变化,要将其重新赋值到成员变量上。而且成员变量有可能在被拷贝之后发生了变化,则拷贝的本地变量已经「过时」了。

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.

创建集合的时候使用字面量表达式,而不是默认构造器,因为字面量表达式还支持扩展表达式和集合 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() 根据类型筛选集合。

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() 转换集合类型,考虑使用以下方法代替:

  • 直接用目标类型创建集合。

  • 遍历元素,对单个元素使用 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.

当使用匿名函数的时候,如果函数调用的方法所需的参数和函数的参数一致,则可以使用 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 中允许使用 : 为参数设置默认值,最好的做法是用 =

// 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.

绝大多数局部变量都不需要有类型标注,而是直接使用 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.

使用箭头表达式定义成员变量,最常见的用法是用于创建 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 关键字:

  • 构造器中引用成员变量;
  • 构造器重定向到某个具名构造器时;
  • 成员变量与局部变量或者参数同名时;

有趣的是,在构造器初始化列表中,可以区分出同名参数,所以不需要使用 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.

在构造器参数前使用 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 构造器中,其中每个参数都是 const 的;
  • 在元数据注解中;
  • const 常量的初始化器;
  • 在 switch..case 表达式中,case 之后 : 之前的值默认也是 const 的;

Error handling

AVOID catches without on clauses.

如果 catch 子句没有使用 on 关键字限定捕捉的异常类型,则会捕捉所有类型的异常,这样我们就没法得到程序出错的具体原因了,到底是 stackoverflow 还是参数异常,又或者是断言条件未满足?所以,哪怕是只捕捉 Exception 也比捕捉全部异常好,Exception 代表程序运行时异常,排除了编码错误造成的异常。

DON’T discard errors from catches without on clauses.

如果你执意捕捉所有异常,请不要丢弃异常内容,至少打印一下好吗?

DO throw objects that implement Error only for programmatic errors.

所有的编码异常都继承自 Error 类,如果是运行时异常造成的错误,则应该抛出运行时异常,尽量在编码异常中排除掉运行时异常。

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

实现了 Error 类的大多是编码异常,这类异常可以告诉我们程序出错的信息,通常不应该捕捉这类异常。

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.

虽然异步场景下优先推荐使用 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 是比较底层的类,应该避免使用,大多数场景下用 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> 作为判断依据。

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.

使用非祈使动词命名布尔类型的属性或变量。布尔类型的变量一般代表某种状态或者能力。

isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup

CONSIDER omitting the verb for a named boolean parameter.

具名参数一般可以省略动词,使用形容词。

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 即会改变对象的内部状态,比如属性等,或者产生一些新数据,或者会引起外部其它对象发生变化等等。

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.

大多数方法都应该直接省略 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.

方法名中不要带有参数相关的信息。

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

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

只有在为了区分多种功能类似的方法时才可以忽略该原则:

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

DO follow existing mnemonic conventions when naming type parameters.

使用现有的助记规则命名类型参数,比如 E 代表集合中的 Element,K/V 代表 Map 的 key/value,R 代表类或方法的 return 值,其它情况使用 T/S/U/N/E 等。

Libraries

Dart 中使用下划线 __ 代表成员是库私有的,这不仅仅是约定俗成的,更是在语法层面做出的规定。

PREFER making declarations private.

对于库的作者而言,这点尤为重要,要尽可能减少暴露接口给库的使用者,只暴露必须使用到的接口。

CONSIDER declaring multiple classes in the same library.

Dart 中每个文件都是一个 library,但是不像 Java 等语言一个文件通常只能代表一个类,Dart 中可以在相同的 library 中包含多个类、顶层变量和方法,只要这些类、变量和方法的确相互联系并且构成一个功能模块就可以了。

将多个类放在一起有诸多好处。因为私有访问权限仅限于库层级,而不是类层级,所以在同一个库中是可以访问其它类中的私有属性和方法的。

Classes and mixins

Dart 是一个纯面向对象的语言,这意味这所有对象都是类的实例。但是另一方面,Dart 有可以像面向过程或者函数式编程一样拥有顶层方法和变量。

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 类来保存全局使用到的一些静态常量。而且为了避免命名冲突,我们常常会使用类名区分相同名字的方法。Dart 中没有这样的限制,相反,Dart 使用 library 作为命名空间,并且在导入包的时候可以使用 as/show/hide 等关键字避免冲突的命名。

我们可以将静态方法或静态成员变量转换成顶层方法或常量,然后以合适的方式命名或者整理进同一个库中。当然,这个规则不一定必须要遵循,一些场合下可能还是使用类和静态成员变量更合适:

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.

同样的,如果一个接口不应该被实现,而使用者却实现了这个接口,那么当未来库的作者对这个接口做任何改动,都会影响到使用者的原有的实现。

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

如果一个接口可以被实现,需要在文档注释中说明。

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 中构造器是一类特殊的方法,用于创建类的实例,它的方法名和类名相同,没有返回值,并且除此之外还可以用 . 分割,后面加标识符,这类构造器叫具名构造器。

CONSIDER making your constructor const if the class supports it.

如果类中的成员变量都是 final 的,而且构造器只是初始化了这些成员变量,那么可以用 const 修饰构造器。这样,使用该类的开发者就可以在需要使用常量的地方使用该类的对象了,比如在其它常量容器中,switch..case 中,默认参数值等等。

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 还是使用方法作为访问某个数据的方式是个微妙但重要的部分。Dart 中成员变量会自动生成 getters,所以访问成员变量和访问 getters,其本质是一样的。所以,通常来说,访问 getters 给人的感觉就像是访问成员变量,它意味着:

  • 这种操作不需要接受参数,但是有返回值;
  • 调用者只关心返回值;
  • 这种操作并不会造成任何用户可见的 side effects;
  • 这种操作具有幂等性,即无论调用多少次,结果相同;
  • 返回的结果对象不会暴露原始对象的所有状态;

如果你的目标操作符合以上所有特点,则可以将这种操作定义成一个 getter 而不是方法。

DO use setters for operations that conceptually change properties.

与 getters 类似,使用 setters 也会遇到类似的困境,不同的是 setters 需要满足 filed-like 特质:

  • 操作只接受一个参数,且不会产生返回值;
  • 操作只会改变对象的某些状态;
  • 操作具有等幂性;

DON’T define a setter without a corresponding getter.

一个可被修改的 setter 往往对应着一个供访问的 getter。

AVOID using runtime type tests to fake overloading.

Dart 中没有重载机制,有的人会定义一个方法,在方法中用 is 判断类型然后根据具体的类型做一些操作。这种操作虽然能达到目的,但是最好的做法还是使用一系列独立的方法,让用户根据不同的类型调用不同的方法。只有当一个对象具体类型不确定,需要在运行时根据不同的类型来调用特定的子类方法的时候,才可以把它们定义在一个方法内。

AVOID public late final fields without initializers.

不像其它 final 成员变量,late final 的成员变量如果没有初始化器的话,Dart 会为它们生成 setters 函数,如果成员变量是 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.

使用级联表达式而不是在方法中返回 this 实现链式调用。

Types

我们使用类型标注限制某部分代码能够使用什么样的值。类型通常出现在两个地方:变量声明处的类型标注 (type annotations) 和使用泛型时的类型参数 (generic invocations)。

类型标注通常就是我们所认为的静态类型,我们可以对变量、参数、属性、返回值使用类型标注,就像下面这个例子:

bool isEmpty(String parameter) { // 返回值和参数的类型标注
  bool result = parameter.isEmpty; // 变量的类型标注
  return result;
}

泛型调用时的类型参数可以是创建集合字面量,或者是调用泛型类的构造方法,或者是调用泛型方法。比如:

var lists = <num>[1, 2]; // 创建集合时指定类型
lists.addAll(List<num>.filled(3, 4)); // 调用泛型类的构造器并指定类型
lists.cast<int>(); // 调用泛型方法

Type inference

类型标注是可选的,因为 Dart 会根据当前上下文推断出具体类型。当缺乏足够的信息推断出类型的时候,Dart 默认会使用 dynamic 类型。这种机制让类型推断看起来是安全的,但实际上使得类型检查完全失效了。

同时拥有类型推断和 dynamic 类型,使得我们在说代码是无类型的 (untyped) 时产生歧义,一个变量到底是动态类型还是没有写类型参数?所以,我们一般不说代码是无类型的,而是使用以下术语代替:

  • 如果代码拥有类型标注,则它的类型即所标注的类型(废话)。
  • 如果代码是推断类型,则说明 Dart 已经确定其类型。而如果类型推断失败,那么我们不把称它为 inferred
  • 如果代码是动态类型的,那么它的静态类型就是 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.

如果变量类型不是显而易见的,也要使用类型标注。

显而易见包括以下这些情况:

  • 字面量,如基本数据类型等
  • 构造器中的参数
  • 引用其它变量或者常量
  • 简单的表达式,比如 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.

给方法的参数添加类型标注,同样很有必要,可以帮助方法的调用者确定参数的边界。

需要注意的是,Dart 不会对可选的参数做类型推断,来源

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 通常可以根据上下文确定匿名方法接收的参数是什么,所以匿名方法一般不需要添加类型标注。

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 允许任何类型的返回值和参数,如果不带签名使用,在有些情况下会导致错误。

// 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.

就像方法可以作为类型标注一样,方法也可以作为参数,并且有特殊的语法支持:

// 函数形式的参数:返回值 Function(参数类型) 参数名
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> 作为返回值。这样可以保证后续的操作,还有支持 await 等。

AVOID using FutureOr<T> as a return type.

如果方法接受 FutureOr<int> 作为参数,那么它可以接收 int 或者 Future<int> 作为参数,这样可以方便调用者用 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.

可选布尔值不但容易让调用着分不清参数的含义,而且容易出错。

// 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.

对于可选位置参数,调用者可能省略中间或者后面部分,尽量把关键部分写在前面,或者使用具名位置参数。

// 调用方可能省略一部分可选位置参数,因此,最重要的写在前面
String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int? end]);

// 使用具名位置参数就没有这个烦恼了
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,使用可选参数代替。

// 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.

当方法接收的参数用数字下标表示范围时,尽量采用前闭后开的习俗,包括开头下标但是不包括结尾的下标。

[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.

如果是可变的对象,比如拥有可变属性的对象,他们的哈希值会随着属性的变化而变化,但是大多数基于哈希的集合没有考虑到这一点,因此,最好不要自定义可变对象的相等性。

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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容

  • 基本数据类型 Number intint a = 1;int b = 0xDEFFFF; doubledouble...
    清無阅读 422评论 0 0
  • 语法学习笔记。资料参考:官方文档(英文)(要直接看英文文档,中文文档可能是机器翻译的,很多地方语句不通顺,埋坑无数...
    SingleDigit阅读 168评论 0 0
  • 如何阅读指南 DO 应始终遵循的准则 DON'T 不应该这么使用的准则 PREFER 应该遵循的准则,但是在某些情...
    _白羊阅读 3,052评论 0 3
  • 彩排完,天已黑
    刘凯书法阅读 4,175评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,609评论 2 7