Dart快速入门 效率篇

参考自Effective Dart,截至2019/06/12

通用原则

类似其他编程语言,有下面两点注意事项:

  • Be consistent, 统一风格
  • Be brief, 保持精简,DRY

最佳实践

指南以下面的关键词开头:

  • ,一定遵守,下面没有前缀的就是以此开头
  • 不要,这么做不是个好主意
  • 推荐,应该遵守,当不遵守时确保有合理理由
  • 避免,和上面相反,除非有足够好的理由,否则不应该这么做
  • 考虑,根据实际情况而定

同时会提到下面这些客体:

  • 库成员,顶级变量、getter、setter、函数
  • 类成员,类变量、getter、setter、函数
  • 成员,库成员或类成员
  • 变量
  • 属性,类中的成员变量、getter、setter,顶级变量、getter、setter

样式

标识符

  • 类名用UpperCamelCase风格
  • 库和文件名用lowercase_with_underscores风格
  • 导入前缀用lowercase_with_underscores风格

    1
    import 'package:javascript_utils/javascript_utils.dart' as js_utils;
  • 其他标识符使用lowerCamelCase风格

  • 推荐使用lowerCamelCase风格命名常量
    • 原因:CAPS_STYLE可读性差/可能会用于final变量/和枚举不搭
  • 把超过2个字母的缩略词当做一般单词来做首字母大写
    • 原因:提高可读性
  • 不要在标识符前加前缀
    • 举例:kTimes

顺序

  • 把”dart:”导入语句放在最前
  • 把”package:”放在相对导入前
  • 推荐把第三方”package:”导入放在其他语句前
  • export语句放在最后
  • 按字母序排序

格式化

  • 使用dartfmt帮你美化
  • 考虑让你的代码更容易美化
  • 避免每行超过80字符
  • 所有控制结构都使用大括号
    • 只有if语句写成1行时可以省略

文档

注释

  • 使用句子的形式表达注释
  • 用单行注释符表达注释

文档注释

  • ///表达文档注释
  • 推荐为公开API书写注释
  • 考虑为私有API书写注释
  • 用一句话为文档注释开头
  • 类似git commit message,第一行后空出一行独立成段
  • 去掉能从上下文直接读出的冗余信息
  • 推荐使用第三人称动词开头表示函数、方法注释
  • 推荐使用名词短语开头表示变量、成员、getter、setter注释
  • 推荐使用名词短语开头表示库、类型注释
  • 考虑在注释中添加示例代码
  • 在注释中用[]方括号引用作用域里的标识符
  • 使用简短平实的语言描述参数、返回值和异常
  • 在注解(annotation)前添加注释

Markdown

Dart允许在comment中使用Markdown格式。

  • 避免滥用markdown
  • 避免使用html格式化文本
  • 推荐使用反引号(```)格式化代码

行文

  • 推荐简洁清晰
  • 避免使用缩写和首字母缩略词
  • 推荐使用“this”而不是“the”来引用实例成员

实践

下面的规则是书写Dart代码时需要知道的指导原则,尤其是维护你类库的人。

  • 出于历史原因,Dart允许通过part of的方式使用库的一部分文件,使用时通过路径而不是变量名引用

    1
    2
    3
    4
    5
    6
    library my_library;
    // good case
    part of "../../my_library.dart";
    // bad case
    part of my_library
  • 不要从库的src文件夹下引用代码

  • 推荐使用相对路径应用库,但是不要跨src文件夹引用

字符串

  • 在长字符串场景下,使用邻接字符串而不是“+”链接

    1
    2
    3
    4
    // good case
    raiseAlarm(
    'ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');
  • 推荐使用插值构造字符串

  • 避免在插值中使用多余的大括号(对于简单的变量)

集合

  • 尽可能使用字面量形式定义集合,必要时提供泛型类型即可

    1
    2
    3
    4
    5
    6
    7
    // good case
    var points = [];
    var userMap = {};
    // bad case
    var points = new List();
    var userMap = new Map();
  • 不使用length属性判断集合是否为空,Dart提供了isEmptyisNotEmpty

  • 考虑使用高阶函数来明确表达你的意图

    1
    2
    3
    var aquaticNames = animals
    .where((animal) => animal.isAquatic)
    .map((animal) => animal.name);
  • 避免Iterable.forEach()中使用函数声明,Dart里的for-in循环可以很好完成该工作,当然函数本身已经定义好除外。

    1
    2
    3
    4
    5
    // good case
    for (var person in people) {
    ...
    }
    people.forEach(print);
  • 使用iterable.toList替代List.from,只在改变list类型时使用List.from

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // Creates a List<int>:
    var iterable = [1, 2, 3];
    // Prints "List<int>":
    print(iterable.toList().runtimeType);
    // Prints "List<dynamic>":
    print(List.from(iterable).runtimeType);
    // Use it with a type
    var numbers = [1, 2.3, 4]; // List<num>.
    numbers.removeAt(1); // Now it only contains integers.
    var ints = List<int>.from(numbers);
  • 使用高级的whereType方法从collection中过滤出特定类型元素

    1
    2
    var objects = [1, "a", 2, "b", 3];
    var ints = objects.whereType<int>();
  • 有类似用法时,不使用cast()方法

    1
    2
    3
    4
    5
    6
    var stuff = <dynamic>[1, 2];
    // Good case
    var ints = List<int>.from(stuff);
    // Bad case
    var ints = stuff.toList().cast<int>();
  • 避免使用cast()方法,用该方法可能更慢且更有风险,通常情况下有下面一些备选方案

    • 创建有正确类型的list
    • 使用每个集合元素时进行casting操作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // Good case
      void printEvens(List<Object> objects) {
      // We happen to know the list only contains ints.
      for (var n in objects) {
      if ((n as int).isEven) print(n);
      }
      }
      // Bad case
      void printEvens(List<Object> objects) {
      // We happen to know the list only contains ints.
      for (var n in objects.cast<int>()) {
      if (n.isEven) print(n);
      }
      }
    • 真正想要强制类型转换时,使用附加类型的List.from

函数

  • 使用函数声明形式命名有名函数(不要使用lambda表达式)
  • 当有有名函数可以完成任务时,不要创建lambda表达式
    1
    2
    3
    4
    5
    6
    7
    // Good case
    names.forEach(print);
    // Bad case
    names.forEach((name) {
    print(name);
    });

参数

  • 使用=分隔入参和它的默认值
  • 不要显式地使用null作为默认值(直接不指定即可)
    1
    2
    3
    void error([String message]) {
    stderr.write(message ?? '\n');
    }

变量

  • 不要显式地使用null初始化变量(语言保证了行为可靠性,不需要再显式设置成null)
  • 不要存储computed value(即可以推算出的值) ,减少冗余信息,保证数据唯一可信源,使用getter和setter去动态推导出它们
  • 考虑忽略局部变量的类型,Dart有强大的静态分析工具帮你推断类型。

成员

  • 不要创建没必要的getter和setter
  • 推荐使用final限定只读属性
  • 考虑使用=>实现只有单一返回语句的函数,对于多行语句建议还是老老实实使用花括号

    1
    2
    3
    get width => right - left;
    bool ready(num time) => minTime == null || minTime <= time;
    containsValue(String value) => getValues().contains(value);
  • 不要使用this.访问成员,除非遇到变量冲突

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // Good case
    class Box {
    var value;
    void clear() {
    update(null);
    }
    void update(value) {
    this.value = value;
    }
    }
    // Bad case
    class Box {
    var value;
    void clear() {
    this.update(null);
    }
    void update(value) {
    this.value = value;
    }
    }
  • 尽可能地在定义变量时初始化该值

构造函数

  • 尽可能使用更简洁的初始化形式

    1
    2
    3
    4
    class Point {
    num x, y;
    Point(this.x, this.y);
    }
  • 不要在初始化形式中定义类型

  • 使用;代替{}表示空方法

    1
    2
    3
    4
    class Point {
    int x, y;
    Point(this.x, this.y);
    }
  • 不要使用可选的new来返回一个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Widget build(BuildContext context) {
    return Row(
    children: [
    RaisedButton(
    child: Text('Increment'),
    ),
    Text('Click!'),
    ],
    );
    }
  • 不要无谓地使用const(基本上const可能出现在所有你能使用new的地方),因为有些语境已经隐式包含了const语义

    • 字面量集合
    • const构造函数调用
    • metadata注解
    • switch的每一个case

异常处理

  • 不要在on以外的语句中丢弃错误,因为在没有on限定时,catch会捕获所有异常
  • 要只在编程错误时抛出Error的异常
  • 不要显式地捕获Error及其子类
  • 使用rethrow重新抛出异常

异步

  • 推荐使用asyncawait提升你的异步代码可读性
  • 只在必要的时候使用async
    • 代码块中使用了await
    • 希望返回一个Future
    • 希望更方便地处理异步中出现的Error
    • 异步事件发生具有先后顺序
  • 考虑使用高阶函数处理stream
  • 避免直接使用Completer
  • Future<T>而不是T判断FutureOr<T>的具体类型

API设计

命名

  • 使用一致的术语
  • 避免缩写,只使用广为人知的缩写
  • 推荐把描述中心词放在最后
  • 考虑尽量让代码看起来像普通的句子
  • 推荐使用名词短语命名非布尔类型的成员或变量
  • 推荐使用非命令式短语命名布尔类型成员或变量
    • 比如配合be动词的不同时态,isEnabled, hasShown
    • 配合助动词,比如hasChildren, canSave
  • 有可能的情况下,考虑省去上一种情况里的动词
  • 推荐使用正向含义的布尔类型变量/方法名
  • 推荐使用命令式动词命名带有副作用的函数和方法
  • 考虑使用名词短语或非命令式动词命名返回数据为主要功能的方法或函数

    1
    2
    list.elementAt(3)
    string.codeUnitAt(4)
  • 考虑使用命令式动词表示你需要对方法所做工作有所关心

  • 避免使用get开头的命名,它通常能用getter代替
  • 推荐使用to___()来命名类型转换
  • 推荐使用as___()来命名类型快照
  • 避免在命名中使用方法、函数的入参
  • 使用助记符命名类型参数
    • E代表集合元素
    • KV代表key和value
    • R代表return type
    • T, SU命名单一通用且上下文表意清晰的泛型
    • 除上面情况外,可以使用完整词汇作为泛型类型名

下划线开头的成员表示成员是私有的,这个特性是内置在Dart语言中的。

  • 推荐使用私有声明,未用_开头的库中的公开声明、顶级定义表示其他库可以访问这些成员,同时也会受到库实现契约的约束。
  • 考虑在同一个库内定义多个类,这样便于在类之间共享私有变量

Dart是纯OOP的语言,它的所有对象都是类实例。当然不像Java,Dart也允许你定义顶级的变量、函数…

  • 避免定义一个函数就可以实现的只有一个实现方法的抽象类

    1
    2
    3
    4
    5
    typedef Predicate<E> = bool Function(E element);
    abstract class Predicate<E> {
    bool test(E element);
    }
  • 避免定义只有静态成员的类,可以使用顶级变量、函数更方便地实现等价效果。当然,如果变量属于一个组,可以这么实现

  • 避免不必要地定义子类
  • 避免实现一个不作为接口的类
  • 避免mixin不设计用作mixin的类
  • 在你的类支持拓展时,定义好文档
  • 在你的类作为接口存在时,定义好文档
  • 在你的类作为mixin存在时,定义好文档

构造函数

  • 考虑在类支持的情况下,让构造函数成为const

成员

  • 考虑尽可能地把成员变量和顶级变量定义为final类型
  • 使用setter和getter定义computed value
  • 不要使用没有getter的setter
  • 避免在返回bool,double,int,num的方法里返回null
  • 避免在方法中返回this,只为了串联调用函数

类型

Dart中的类型可以帮助使用者理解你API中的静态类型设计,它分两种:类型注解和类型参数。前一种放在变量名前注解变量类型,后一种作为泛型参数传入。

1
2
3
4
5
6
bool isEmpty(String parameter) {
bool result = parameter.length == 0;
return result;
}
List<int> ints = [1, 2];

在未指定类型时,Dart会从上下文自动推断或者使用缺省的dynamic类型。

简言之,Dart提供了强大的类型推导简化了你声明类型的负担,但同时不声明类型会降低API的可读性,下面一些guideline帮你在两点间找到一个平衡。

  • 推荐对于类型表意不清晰的public属性和顶级变量使用类型注解

    1
    2
    3
    Future<bool> install(PackageId id, String destination) => ...
    const screenWidth = 640; // Inferred as int.
  • 考虑对于类型表意不清晰的private属性添加类型注解

  • 避免为局部变量添加类型注解,如果你需要静态类型提供的便利,可以借助is限制变量类型
  • 避免在方法表达式上使用类型,考虑到方法表达式通常作为方法入参,类型可以自动推断,不需要类型注解
  • 避免冗余的泛型和类型注解

    1
    2
    3
    4
    5
    // Good case
    Set<String> things = Set();
    // Bad case
    Set<String> things = Set<String>();
  • 在不希望使用Dart推断的类型时,使用类型注解

  • 推荐使用显示的dynamic代替Dart推断失败回退的dynamic
  • 推荐在Function类型注解中添加函数类型签名
  • 不要为setter指定返回值
  • 使用新式的typeof判断类型

    1
    typedef Comparison<T> = int Function(T, T);
  • 使用Object代替dynamic表示可以接受任何对象

  • 使用Future<void>作为无返回值的异步函数返回类型
  • 不使用FutureOr<T>作为返回值

参数

  • 避免位置参数作为可选布尔参数,这样可读性比较差

    1
    2
    3
    4
    5
    6
    7
    // Bad case
    new Task(true);
    new Task(false);
    new ListBox(false, true, true);
    new Button(false);
    // Good case
  • 避免将用户想忽略的参数放在位置可选参数的前列

  • 避免使用强制的无意义的参数

    1
    2
    // Bad case
    string.substring(start, null)
  • 使用左闭右开区间表示两个参数代表的范围

相同判断

  • 覆写==的同时覆写hashCode,默认的哈希函数实现了恒等式哈希。任何两个相等的两个对象必须具有相同的哈希值
  • ==需要遵循数学的相等规则
    • 自反,a == a
    • 对称,a == b => b == a
    • 传递,a == b && b == c => a == c
  • 避免为可变对象自定义相等函数,hashCode函数会增加你的工作量
  • 不要在自定义==中判断null,Dart也已经替你做了这部分工作