flutter自学笔记5- dart 编码规范

0 阅读13分钟

笔记5;本文仅梳理了dart.cn文档的 Dart语言编写代码时应该遵循的风格规范,包括标识符命名、Linter规则应用、库和文件命名、代码格式化等方面的内容。 简化了一些内容,方便以后复习使用

以下是具体内容:

1、命名

命名基本要求

在 Dart(以及大多数其他编程语言)中,变量命名是受到一定规则限制的,这些规则通常规定了哪些字符是允许的,哪些是不允许的。

对于 Dart 来说,变量名必须:

  1. 以字母(A-Z 或 a-z)或下划线(_)开头。

  2. 后续字符可以是字母、数字(0-9)或下划线。

  3. 不能包含任何特殊字符,如 !@#$(除了作为字符串插值的一部分外)、%^&*()-+={}[]|\:;'",.<>/?等。

**大驼峰命名 UpperCamelCase **:

Classes(类名)元数据注解的类、扩展名、 enums(枚举类型)、 typedefs(类型定义)、以及 type parameters(类型参数)应该把每个单词的首字母都大写(包含第一个单词),不使用分隔符。

class SliderMenu { ... }
class HttpRequest { ... }
typedef Predicate<T> = bool Function(T value);

// 使用元数据注解的类
@Foo(anArg)
class A { ... }

// 扩展名
extension MyFancyList<T> on List<T> { ... }

特殊两字缩写:

// 两个字母,用英文大写,在标识符中大写
ID // "identifier"
TV // "television"
UI // "user interface"

小驼峰命名 lowerCamelCase

类成员、顶级定义、变量、参数以及命名参数等 使用小写字母开头,后续单词首字母大写,

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

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

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

小驼峰下划线命名 lowercase_with_underscores

常量、库名、文件名等使用小写字母和下划线组合的方式命名,如piurl_schemeangular_components

库:

my_package
└─ lib
   └─ file_system.dart
   └─ slider_menu.dart

引用:

import 'dart:math' as math;
import 'package:angular_components/angular_components.dart' as angular_components;
import 'package:js/js.dart' as js;

自定义的缩写:

// 比两个字母长,自定义的缩写,所以总是像一个单词:
Http // "hypertext transfer protocol"
Nasa // "national aeronautics and space administration"
Uri // "uniform resource identifier"
Esq // "esquire"
Ave // "avenue"

// Two letters, not capitalized in English, so like a word in an identifier:
Mr // "mister"
St // "street"
Rd // "road"

前导下划线

Dart在标识符中使用前导下划线将成员和顶级声明标记为私有

_name // 私有的命名

默认public,不需要设置

对于局部变量、参数、局部函数或库前缀,没有“私有”的概念。

常量命名

在需要与现有代码风格保持一致或生成与Java并行代码时,可以使用全大写字母加下划线的方式命名常量。但一般情况下,推荐使用lowerCamelCase风格。

匈牙利命名法(dart中不建议)

在编译器无法帮助你了解自己代码的时,匈牙利命名法可以很好判断类型,但是因为 Dart 可以提示你声明的类型,范围,可变性和其他属性,所以没有理由在标识符名称中对这些属性进行编码。

命名原则(dart中不建议)

  1. 属性:表示变量的属性或作用域,例如全局变量(g_)、常量(c_)、C++类成员变量(m_)、静态变量(s_)等。
  2. 类型:表示变量的数据类型,例如数组(a)、指针(p)、函数(fn)、无效值(v)、句柄(h)、长整型(l)、布尔型(b)、浮点型(f)、双字(dw)、字符串(sz)、短整型(n)、双精度浮点(d)、计数(c,通常用cnt)、字符(ch,通常用c)、整型(i,通常用n)、字节(by)、字(w)、实型(r)、无符号(u)等。
  3. 对象描述:表示变量的具体对象或用途,例如最大(Max)、最小(Min)、初始化(Init)、临时变量(T或Temp)、源对象(Src)、目的对象(Dest)等。

命名不可过长

单行尽量控制在80字符内

要使用一致的术语

// bad
renumberPages() //与pageCount不同。
convertToSomething() //与toX()前例不一致。
wrappedAsSomething() //与asX()前例不一致。

// good
pageCount //字段。
updatePageCount() //与pageCount一致。
toSomething() //与Iterable的toList()一致。
asSomething() //与List的asMap()一致。

使用广为人知的缩写

// bad
InputOutputStream
HypertextTransferProtocolRequest

// good
IOStream
HttpRequest

最具描述性的名词放到最后

描述Rule的命名:

// bad
RuleFontFaceCss           // Not a CSS

// good
CssFontFaceRule // CSS中字体的规则

让代码看起来像普通的句子

// bad
if (error.empty) ... // 告诉错误清空自己,还是问它是否存在?

// good
if (error.isEmpty) ...

不建议使用动词类型的变量和属性

// bad
list.deleteItems // delete 是动作,适合为Action 方法命名,不适合变量名称命名

// good
list.deletedItems

使用非命令式动词短语命名布尔类型的变量和属性。

// bad
empty //形容词还是动词?
withElements //听起来可能包含元素。
closeable //听起来像一个接口。

// good
isEmpty
hasElements
canClose

考虑省略命名布尔参数的动词

// bad
Isolate.spawn(entryPoint, message, isPause: false);

// good
Isolate.spawn(entryPoint, message, paused: false);

布尔属性或变量本身存在肯定的含义,再取“肯定”含义的前后缀名字

// bad
socket.isDisconnected

// good
socket.connected

推荐使用命令式动词短语来命名带有副作用的函数或者方法

// good
list.add('element');
queue.removeFirst();
window.refresh();

考虑使用名词短语或者非命令式动词短语命名返回数据为主要功能的方法或者函数

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

考虑使用命令式动词短语命名一个函数或方法,若果你希望它的执行能被重视

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

避免在方法命名中使用 get 开头

getter 方法名称中应该移除 get, 默认是get含义。

推荐使用 to___() 来命名把对象的状态转换到一个新的对象的函数

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

推荐使用 as___() 来命名把原来对象转换为另外一种表现形式的函数

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

避免在方法或者函数名称中描述参数

// bad
list.addElement(element) // 命名元素重复
map.removeKey(key)

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

要在命名参数时,遵循现有的助记符约定

// E 用于集合中的 元素 类型:
class List<E> {}
class HashSet<E> {}

// K 和 V 分别用于关联集合中的 key 和 value 类型:
class Map<K, V> {}

//表达泛型含义,请使用T,S 和 U
class Future<T> {
  Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
}

// 节点Node
final List<Node> nodes = [];

// 边距 
final List<Edge> edges = [];

导入库

要在 part of 中使用字符串

// bad
part of my_library;

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

不要导入 package 中 src 目录下的库

lib 下的 src 目录为 package 自己实现的私有库

不要让导入路径进入或超出lib目录

不要在导入路径中使用/lib/。

不要使用../来跳出lib目录。

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

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

优先使用相对导入路径

package 目录结构:

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

1、在 lib/api.dart 文件中导入 utils.dart ,应该这样使用:

import 'src/utils.dart';

2、在 lib/src/utils.dart 文件中导入同级 stuff.dart ,上一级的 lib/api.dart 应该这样使用:

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

3、在test/api_test.dart 文件中导入不同目录的 lib/api.dart 应该这样使用:

import 'package:my_package/api.dart'; // 不要使用 'lib'.

导入顺序

1、"dart:" 导入语句放到其他导入语句之前
2、把 "package:" 导入语句放到项目相关导入语句之前
3、把导出 (export) 语句作为一个单独的部分放到所有导入语句之后
4、按照字母顺序来排序每个部分中的语句
// "dart:" 导入在最开头
import 'dart:async';

// package 排第二
import 'package:bar/bar.dart'; // package中字母顺序
import 'package:foo/foo.dart'; // package中字母顺序

// 项目相关导入
import 'util.dart';

// 其他

// 最后的是export
export 'src/error.dart';

空值 Null

不要显式地将变量初始化为空值


// bad
Item? bestItem = null;

// good
Item? bestItem;

不要使用显式的默认空值

// bad
void error([String? message = null]) {

}

// good
void error([String? message]) {

}

不要在等式运算中使用truefalse

// bad
if (nonNullableBool == true) { ... }

// good
if (nonNullableBool) { ... }

避免在需要检查变量是否已初始化时使用late变量

在 Dart 编程语言中,late 关键字用于声明一个非空的延迟初始化变量。这意味着你可以在声明变量时不立即初始化它,但必须在变量使用之前对其进行初始化。虽然 late 关键字在某些情况下可以提供便利,但过度使用或者在不适当的场合使用它可能会导致代码的可读性和维护性下降,特别是在需要检查变量是否已初始化的情况下。

// bad

class Example {
  late String data;

  void loadData() {
    // 假设这里是从外部源加载数据
    data = "数据加载了";
  }

  void printData() {
    print(data);
  }
}

void main() {
  var example = Example();
  example.printData(); // 这将会抛出异常,因为 data 还没有被初始化
  
  example.loadData();
  example.printData(); // 正确输出 "Loaded Data"
}


// good

class Example {
  String? data;

  void loadData() {
    data = "数据加载了";
  }

  void printData() {
    print(data ?? "数据尚未加载.");
  }
}

考虑使用类型提升或空值检查模式来处理可为空类型

// bad
print(response!.url);

// good
print(response?.url);
print(response?.url ?? 'url 为空');

字符串

要使用相邻字符串的方式连接字面量字符串

如果你有两个字面量字符串(不是变量),你不需要使用 + 来连接它们。只需要将它们挨着在一起就可以了

// bad
print('first' + 'name');

// good
print('first' 'name');

推荐使用插值的形式来组合字符串和值

如果有变量,不建议使用 + 来连接它们

// bad
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

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

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

${} 中是表达式、或${}外需要拼接内容时

// bad
var greeting = 'Hi, ${name}, I love your ${decade}s costume.';

// good (逗号是特殊字符,非变量名一部分)
var greeting = 'Hi, $name, I love your ${decade}s costume.';

集合

原生支持了四种类型:list, map, queue,和 set

要尽可能的使用集合字面量

// bad(没必要明确指出类型)
var addresses = Map<String, Address>();
var counts = Set<int>();

// good (自动推断)
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};

不要使用 .length 来判断一个集合是否为空

使用 getter 函数:.isEmpty.isNotEmpty 判空

// bad
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

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

避免在 Iterable.forEach() 中使用字面量函数

// bad (简单字面参数不建议使用 forEach)
people.forEach((person) {
  ...
});

// good (建议使用 for-in)
for (final person in people) {
  ...
}

在 Dart 中,forEach 方法和 for-in 循环都可以用来遍历集合(如列表或集合),但它们有一些重要的区别,这些区别影响了它们的适用场景和代码风格。

void printFruit(String fruit) {
  print('Fruit: $fruit');
}

List<String> fruits = ['apple', 'banana', 'cherry'];
fruits.forEach(printFruit);
  1. forEach 是一个高阶函数,它接受一个函数作为参数。这种风格更符合函数式编程的范式。

  2. 匿名函数或lambda表达式:在 forEach 的调用中,你通常会传递一个匿名函数(也称为 lambda 表达式)来定义对每个元素的操作。

  3. 不可变性:由于 forEach 不返回任何值(返回类型是 void),它通常用于那些不需要基于遍历结果进一步计算的场景。

  4. 只读访问:在 forEach 的 lambda 表达式中,你通常不会修改外部变量或集合本身,因为它鼓励的是对集合元素的只读访问。

  5. 性能考虑:在某些情况下,由于 Dart 的闭包和函数调用的开销,forEach 可能会比 for-in 循环稍微慢一些,尽管这种差异在大多数情况下是可以忽略不计的。

不要混淆使用 List.from() 和 toList()

// Creates a List<int>:
var iterable = [1, 2, 3];

// 1、如果不需要改变原始对象的类型 .toList()
print(iterable.toList().runtimeType); // Prints "List<int>":


// 2、改变原始对象的类型 .from
print(List.from(iterable).runtimeType);

要使用 whereType() 按类型过滤集合

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

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

不要使用 cast(),如果有更合适的方法

cast() 方法返回一个惰性集合 (lazy collection) ,每个操作都会对元素进行检查。如果只对少数元素执行少量操作,那么这种惰性方式就非常合适。但在许多情况下,惰性验证和包裹 (wrapping) 所产生的开销已经超过了它们所带来的好处。

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

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

// bad
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();

// good
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);

函数

要使用函数声明的方式为函数绑定名称

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

	// good
	var localFunction() {
    ...
  };
}

不要使用 lambda 表达式来替代 tear-off

应该避免在可以通过直接引用( tear-off)方法来达到目的的情况下,不必要地使用 lambda 表达式(也称为匿名函数或闭包)

// bad
List<String> fruits = ['apple', 'banana', 'cherry'];
fruits.forEach((String fruit) => print(fruit));

// good
List<String> fruits = ['apple', 'banana', 'cherry'];
fruits.forEach(print);

推荐为类型不明显的公共字段和公共顶级变量指定类型注解

// bad
install(id, destination) => ... // id 类型不明
  
// good
Future<bool> install(PackageId id, String destination) => ... // id 类型不明,指明类型
  
const screenWidth = 320; // 类型明确,自动推断为 int

避免为初始化的局部变量添加冗余地类型注解

// bad
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  List<List<Ingredient>> desserts = <List<Ingredient>>[];
  for (final List<Ingredient> recipe in cookbook) {
    if (pantry.containsAll(recipe)) {
      desserts.add(recipe);
    }
  }

  return desserts;
}

// good
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  // 使用 where 和 containsAll 方法来过滤出所有可用食谱
  return cookbook.where((recipe) => pantry.containsAll(recipe)).toList();
}

要在函数声明上注解返回类型

// bad
makeGreeting(String who) {
  return 'Hello, $who!';
}

// good
String makeGreeting(String who) {
  return 'Hello, $who!';
}

要在函数声明中注释参数类型

// bad
void sayRepeatedly(message, {count = 2}) {
  ...
}

// good
void sayRepeatedly(String message, {int count = 2}) {
  ...
}

避免在函数表达式上注解推断的参数类型

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

// good
var names = people.map((person) => person.name); // 不需要显式Person

不要在初始化形式时键入注释

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

要在泛型调用中写入未推断的类型参数

// bad
var playerScores = {};

// good
var playerScores = <String, int>{};

不要在推断的泛型调用上编写类型参数

// bad
final Completer<String> response = Completer<String>();

// good
final Completer<String> response = Completer(); // 从前面的Completer<String> 可以推断泛型类型,初始化时直接使用Completer()

避免编写不完整的泛型类型

// bad
var completer = Completer<Map>();

// good
var completer = Completer<Map<String, int>>();

推荐使 function 类型注解的特征更明显

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

void main() {
  bool isEmpty(String str) => str.isEmpty;
  
  print(isValid("", isEmpty)); // 输出: true
  print(isValid("Hello", isEmpty)); // 输出: false
}

bool Function(String) test:第二个参数,一个函数,这个函数接受一个 String 类型的参数并返回一个 bool 类型的值。

不要为 setter 方法指定返回类型

// bad
void set foo(Foo value) { ... }

// good
set foo(Foo value) { ... } // 不显式返回 void

不要使用弃用的 typedef 语法

// bad 旧的语法(Dart 2.12 之前)
typedef int Comparison<T>(T a, T b);

// good 新的语法(Dart 2.12 及以后)
typedef Comparison<T> = int Function(T a, T b);

变量

对局部变量的var和final遵循一致的规则

对于不再被重新赋值的局部变量使用 final,对于需要重新赋值的局部变量使用 var

避免保存可计算的结果

设计圆形类,含半径,面积和周长

// bad
class Circle {
  // 实现getter 和 setter 方法
  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;
  }
}

// good
class Circle {
  double radius;

  Circle(this.radius);

  // 用时计算
  double get area => pi * radius * radius;
  double get circumference => pi * 2.0 * radius;
}

不要 使用 == 操作符与可空值比较

  1. 潜在的 NullPointerException:虽然 Dart 的 == 操作符在比较时会优雅地处理 null(即 null == null 会返回 true,而 null == anyNonNullValue 会返回 false),但如果你在不检查变量是否为 null 的情况下就进行比较,可能会掩盖代码中的其他问题。
  2. 代码可读性:直接与可空值比较可能会使代码的逻辑变得复杂,难以理解和维护。明确处理 null 值可以提高代码的可读性和健壮性。
  3. 逻辑错误:如果不小心将 == 与可空值一起使用,可能会导致逻辑错误。例如,你可能期望一个值不为 null 时才进行比较,但实际上该值可能为 null,从而导致意外的结果。
String? str1 = "hello";
String? str2 = null;

// 安全的比较方式
if (str1 != null && str2 != null && str1 == str2) {
  // do something
} else {
  // handle the case where either str1 or str2 is null, or they are not equal
}

// 或者使用空合并操作符和默认值
bool areEqual = (str1 ?? "") == (str2 ?? "");

成员

不要为字段创建不必要的 getter 和 setter 方法

// bad
class Box {
  // 实现getter 和 setter 方法,但是没有特殊逻辑
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

// good
class Box {
  Object? contents; // 语法默认支持的set get
}

不要 在没有对应的 getter 的情况下定义 setter

属性可以被写入但无法读取,会令人感到困惑

推荐使用 final 关键字来创建只读属性

如果一个变量对于外部代码来说只能读取不能修改,最简单的做法就是使用 final 关键字来标记这个变量

考虑对简单成员使用 =>

// good
class Circle {
  double radius;

  Circle(this.radius);

  // 面积和周长
  double get area => pi * radius * radius;
  double get circumference => pi * 2.0 * radius;
}

没有歧义时可不使用 this. (在重定向命名函数和避免冲突的情况下除外)

class Box {
  Object? value;

  void clear() {
    // bad
    this.update(null);
    
    // good
    update(null);
  }

  void update(Object? value) {
    // 这里使用this是必要的,因为局部变量value和成员变量命名value一样,添加this区分
    this.value = value;
  }
}

定义变量的初始化变量值

初始化变量有几个好处:

  1. 避免空值:通过初始化变量,可以防止变量在后续使用中出现 null 值,这有助于减少空指针异常(NullPointerException)的风险。
  2. 明确意图:初始化变量可以使代码更加清晰,因为它明确指出了变量的预期用途和初始状态。
  3. 代码健壮性:初始化的变量减少了在代码后续部分中需要进行的空值检查,从而使代码更加健壮和易于维护。
  4. 立即可用性:一旦变量被初始化,它就可以立即被使用,而无需担心它是否已经被赋值。

在 Dart 中,有几种方式可以在定义变量时初始化它们:

在声明时直接赋值

var number = 42;
String name = "Alice";

使用构造函数参数(在类中):

class Person {
  String name;
  int age;
 
  Person(this.name, this.age);
}
 
var person = Person("Bob", 30);

使用默认构造函数参数(在类中,为可选参数提供默认值):

class Rectangle {
  double width;
  double height;
 
  Rectangle({this.width = 1.0, this.height = 1.0});
}
 
var square = Rectangle(); // width 和 height 默认为 1.0

使用 late 关键字延迟初始化(适用于需要在构造函数体中初始化的场景,但希望保持变量不可为空):

class Circle {
  late double radius;
 
  Circle(double initialRadius) {
    radius = initialRadius;
  }
}

然而,请注意,late 关键字仅在你确信变量将在首次使用前被初始化时才应使用。

使用计算属性(对于可以根据其他变量动态计算的值):

class Circle {
  double radius;
 
  Circle(this.radius);
 
  double get area => pi * radius * radius;
}

在这种情况下,area 不是一个在定义时初始化的变量,而是一个每次访问时都会重新计算的属性。

使用 .. 实现方法级联,而不是从方法中返回 this

// bad :通过方法返回自身,实现“方法级联”
var buffer = StringBuffer()
    .write('one')
    .write('two')
    .write('three');

// good:使用 `..` 实现方法级联
var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');

构造函数

要用 ; 来替代空的构造函数体 {}

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

不要使用 new

在你的代码中弃用和避免使用 new, 在 dart 2 中已过期。


// bad
Widget build(BuildContext context) {
  return new Row(
    ...
  );
}

// good
Widget build(BuildContext context) {
  return Row(
    ...
  );
}

不要冗余地使用 const

表达式一定是常量的上下文中,const 关键字是隐式的,不需要写,它能自己推断。

// bad(他就是不可变的,不需要显式说明 const )
const primaryColors = const [
  const Color('red', const [255, 0, 0]),
  const Color('green', const [0, 255, 0]),
  const Color('blue', const [0, 0, 255]),
];

// good
const primaryColors = [
  Color('red', [255, 0, 0]),
  Color('green', [0, 255, 0]),
  Color('blue', [0, 0, 255]),
];

错误处理

避免使用没有 on 语句的 catch

// bad
try {
  // 可能会抛出异常的代码
} catch (e) {
  // 处理异常
}


// good
try {
  // 可能会抛出多种类型异常的代码
} on FormatException catch (e) {
  // 处理 FormatException
} on IOException catch (e) {
  // 处理 IOException
} catch (e) {
  // 处理所有其他类型的异常(仍然不推荐,除非必要)
}

不要丢弃没有使用 on 语句捕获的异常

// 没有on语句,任然保留异常捕获逻辑:
try {
  // 可能会抛出异常的代码
} catch (e) {
  // 处理异常
}

要只在代表编程错误的情况下才抛出实现了 Error 的异常

编程错误

当代码中存在逻辑错误,或者程序的某个部分没有按预期工作时,应该抛出 Error 异常。这有助于开发者在开发和测试阶段发现并修复问题。

不可恢复的错误

如果某个错误是无法恢复的,即程序无法继续执行其当前任务,那么应该抛出 Error 异常。例如,如果尝试访问一个不存在的文件,并且这个操作是程序继续执行所必需的,那么可以抛出一个 FileNotFoundError(假设这是一个 Error 的子类)。

不要用于控制流程

不应该使用 Error 异常来控制程序的正常流程。例如,不应该在用户输入无效数据时抛出异常,而应该使用验证逻辑来处理这种情况,并向用户提供清晰的反馈。

提供有用的信息

当抛出 Error 异常时,应该提供足够的信息来帮助开发者理解错误的原因和位置。这通常包括错误消息、堆栈跟踪和任何相关的上下文信息。

自定义异常

如果需要表示特定类型的错误,可以创建自定义的 Error 子类。这有助于区分不同类型的错误,并为每种错误提供特定的处理逻辑。

避免捕获并忽略

不要捕获 Error 异常而不进行处理或仅仅记录日志后就忽略它。这可能会导致程序在不一致的状态下继续运行,从而引发更多的错误。 使用 throw 关键字:在 Dart 中,使用 throw 关键字来抛出异常。例如,throw new MyCustomError("Something went wrong!");(注意,Dart 的新语法中可能不需要使用 new 关键字)。

考虑使用其他异常类型

对于不是编程错误的情况,比如用户输入错误或业务逻辑错误,可以考虑使用 Exception 类或其子类来表示这些异常。这些异常通常用于表示可以通过用户操作或业务逻辑来处理的错误。

要使用 rethrow 来重新抛出捕获的异常

try {
  somethingRisky();
} catch (e) {
  // bad
  if (!canHandle(e)) throw e; // throw 会把异常堆栈信息重置为最后抛出的位置
  
  // good
  if (!canHandle(e)) rethrow; // rethrow 保留了原来的异常堆栈信息
  
  handle(e);
}

异步

推荐使用 async/await 而不是直接使用底层的特性

在 Dart 中,Completer 类是 dart:async 库的一部分,它用于在异步编程中手动完成一个 FutureCompleter提供了一个 complete 方法,该方法可以用来设置 Future 的结果值,以及一个 completeError 方法,该方法可以用来设置 Future 的错误结果。

// bad

import 'dart:async';

Future<String> fetchData() async {
  // 创建一个 Completer 实例
  final completer = Completer<String>();

  // 假设我们有一个异步操作,比如从网络获取数据
  // 这里我们用 Timer 来模拟一个异步延迟
  Timer.run(() {
    // 模拟数据获取成功
    String data = "Fetched data";
    // 使用 completer 来完成 Future,并设置结果值
    completer.complete(data);

    // 如果数据获取失败,你可以使用 completeError 方法来设置错误结果
    // 例如:completer.completeError(new Exception("Failed to fetch data"));
  });

  // 返回 Completer 的 future 属性,这个属性是一个 Future 对象
  return completer.future;
}

void main() async {
  try {
    // 调用 fetchData 函数,并等待它的结果
    String result = await fetchData();
    print("Data: $result");
  } catch (e) {
    // 处理错误
    print("Error: $e");
  }
}

可以使用 await 、async 替换 Completer :

// good

import 'dart:async';

Future<String> fetchData() async {
  // 模拟一个异步操作,比如从网络获取数据
  // 使用 await 和 Timer.delayed 来模拟异步延迟
  await Timer.delayed(Duration(seconds: 1)); // 假设网络请求需要1秒

  // 模拟数据获取成功
  String data = "Fetched data";

  // 直接返回数据,因为 fetchData 是 async 函数,它自动返回一个 Future
  return data;
}

void main() async {
  try {
    // 调用 fetchData 函数,并使用 await 来等待它的结果
    String result = await fetchData();
    print("Data: $result");
  } catch (e) {
    // 处理错误(在这个例子中,不会有错误,因为 fetchData 不抛出异常)
    print("Error: $e");
  }
}

要使用 Future<T> 对 FutureOr<T> 参数进行测试,以消除参数可能是 Object 类型的歧义

Future<T> logValue<T>(FutureOr<T> value) async {
  // bad
  if (value is T) {
    // do something
  }
  
  // good
  if (value is Future<T>) {
    // do something
  }
  
  return value;
}

参考:

在线验证代码

dart文档:https://dart.cn/guides/