代码示例
最后更新于:2022-04-02 05:55:34
[TOC]
## Hello World
每个应用都有一个main()功能。要在控制台上显示文本,您可以使用顶级print()功能:
~~~
void main() {
print('Hello, World!');
}
~~~
## 变量
即使在类型安全的Dart代码中,由于类型推断,大多数变量都不需要显式类型:
~~~
var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
'tags': ['saturn'],
'url': '//path/to/saturn.jpg'
};
~~~
了解更多关于在DART变量,包括默认值,final和const关键字,和静态类型。
## 控制流程语句
Dart支持通常的控制流语句:
~~~
if (year >= 2001) {
print('21st century');
} else if (year >= 1901) {
print('20th century');
}
for (var object in flybyObjects) {
print(object);
}
for (int month = 1; month <= 12; month++) {
print(month);
}
while (year < 2016) {
year += 1;
}
~~~
## 函数
我们建议 指定每个函数的参数类型和返回值:
~~~
int fibonacci(int n) {
if (n == 0 || n == 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
var result = fibonacci(20);
~~~
对于包含单个语句的函数,简写=>(胖箭头)语法很方便。将匿名函数作为参数传递时,此语法特别有用:
~~~
flybyObjects.where((name) => name.contains('turn')).forEach(print);
~~~
除了显示匿名函数(参数where())之外,此代码还显示您可以使用函数作为参数:顶级print()函数是参数forEach()。
## 注释
Dart评论注释通常以//。
~~~
// This is a normal, one-line comment.
/// This is a documentation comment, used to document libraries,
/// classes, and their members. Tools like IDEs and dartdoc treat
/// doc comments specially.
/* Comments like these are also supported. */
~~~
## import
要访问其他库中定义的API,请使用import。
~~~
// Importing core libraries
import 'dart:async';
import 'dart:math';
// Importing libraries from external packages
import 'package:test/test.dart';
// Importing files
import 'path/to/my_other_file.dart';
import '../lib/samples/spacecraft.dart';
~~~
阅读有关Dart中的库和可见性的更多信息,包括库前缀,show以及hide通过deferred关键字延迟加载。
## 类
这是一个具有三个属性,两个构造函数和一个方法的类的示例。其中一个属性无法直接设置,因此使用getter方法(而不是变量)定义。
~~~
class Spacecraft {
String name;
DateTime launchDate;
// Constructor, with syntactic sugar for assignment to members.
Spacecraft(this.name, this.launchDate) {
// Initialization code goes here.
}
// Named constructor that forwards to the default one.
Spacecraft.unlaunched(String name) : this(name, null);
int get launchYear =>
launchDate?.year; // read-only non-final property
// Method.
void describe() {
print('Spacecraft: $name');
if (launchDate != null) {
int years =
DateTime.now().difference(launchDate).inDays ~/
365;
print('Launched: $launchYear ($years years ago)');
} else {
print('Unlaunched');
}
}
}
~~~
您可以使用这样的Spacecraft类:
~~~
var voyager = Spacecraft('Voyager I', DateTime(1977, 9, 5));
voyager.describe();
var voyager3 = Spacecraft.unlaunched('Voyager III');
voyager3.describe();
~~~
阅读有关Dart中类的更多信息,包括初始化列表,可选new和const重定向构造函数, factory构造函数,getter,setter等等。
## 继承
Dart有单一继承。
~~~
class Orbiter extends Spacecraft {
num altitude;
Orbiter(String name, DateTime launchDate, this.altitude)
: super(name, launchDate);
}
~~~
阅读更多关于类的扩展,可选的@override注释,等等
## Mixins
Mixins是一种在多个类层次结构中重用代码的方法。以下类可以充当mixin:
~~~
class Piloted {
int astronauts = 1;
void describeCrew() {
print('Number of astronauts: $astronauts');
}
}
~~~
要将mixin的功能添加到类中,只需使用mixin扩展类。
~~~
class PilotedCraft extends Spacecraft with Piloted {
// ···
}
~~~
Orbiter现在有了astronauts字段和describeCrew()方法。
## 接口和抽象类
Dart没有接口关键字。相反,所有类都隐式地定义一个接口。因此,您可以实现任何类。
~~~
class MockSpaceship implements Spacecraft {
// ···
}
~~~
您可以创建一个抽象类,由一个具体类扩展(或实现)。抽象类可以包含抽象方法(带有空主体)。
~~~
abstract class Describable {
void describe();
void describeWithEmphasis() {
print('=========');
describe();
print('=========');
}
}
~~~
任何类扩展描述都有describe()方法,它调用extender的describe()实现。
## 异步
避免回调地狱,并通过使用异步和wait使代码更具可读性。
~~~
const oneSecond = Duration(seconds: 1);
// ···
Future printWithDelay(String message) async {
await Future.delayed(oneSecond);
print(message);
}
~~~
上述方法相当于:
~~~
Future printWithDelay(String message) {
return Future.delayed(oneSecond).then((_) {
print(message);
});
}
~~~
如下一个示例所示,async和await会帮助使异步代码易于阅读。
~~~
Future createDescriptions(Iterable objects) async {
for (var object in objects) {
try {
var file = File('$object.txt');
if (await file.exists()) {
var modified = await file.lastModified();
print(
'File for $object already exists. It was modified on $modified.');
continue;
}
await file.create();
await file.writeAsString('Start describing $object in this file.');
} on IOException catch (e) {
print('Cannot create description for $object: $e');
}
}
}
~~~
您还可以使用async*,它提供了一种很好的、可读的方式来构建流。
~~~
Stream report(Spacecraft craft, Iterable objects) async* {
for (var object in objects) {
await Future.delayed(oneSecond);
yield '${craft.name} flies by $object';
}
}
~~~
## 异常
要引发异常,使用throw:
~~~
if (astronauts == 0) {
throw StateError('No astronauts.');
}
~~~
要捕获异常,使用带有on或catch(或两者)的try语句:
~~~
try {
for (var object in flybyObjects) {
var description = await File('$object.txt').readAsString();
print(description);
}
} on IOException catch (e) {
print('Could not describe object: $e');
} finally {
flybyObjects.clear();
}
~~~
注意,上面的代码是异步的;尝试在异步函数中同时工作同步代码和代码。
';
相等
最后更新于:2022-04-02 05:55:32
[TOC]
为类实现自定义相等行为可能很棘手。用户对对象需要匹配的等式如何工作有很深的直觉,而像哈希表这样的集合类型有一些微妙的约定,他们希望元素遵循这些约定。
## 如果您重写==,请重写hashCode。
默认的哈希代码实现提供了一个标识哈希——如果两个对象是完全相同的对象,通常只有相同的哈希代码。同样,==的默认行为是identity。
如果你重写==,这意味着你可能有不同的对象被你的类认为是“相等的”。任何两个相等的对象都必须具有相同的哈希代码。否则,映射和其他基于散列的集合将无法识别这两个对象是等价的。
## 一定要让你的==运算符遵守等式的数学规则。
一个等价关系应该是:
* 条件反射:a == a应该总是返回true。
* 对称的:a == b应该返回与b == a相同的东西。
* 传递式:如果a == b和b == c都返回true,那么a == c也应该返回true。
使用==的用户和代码期望遵循所有这些规则。如果您的类不能遵守这些规则,那么==就不是您要表示的操作的正确名称。
## 避免为可变类定义自定义等式。
在定义==时,还必须定义hashCode。这两个都应该考虑到对象的字段。如果这些字段改变了,那么就意味着对象的哈希代码可以改变。
大多数基于哈希的集合都没有预料到这一点——它们假设一个对象的哈希代码将永远是相同的,如果这不是真的,那么它的行为可能是不可预测的。
## 不要在自定义 ==运算符中检查null。
该语言指定此检查是自动完成的,只有当右侧不为空时才调用您的==方法。
~~~
class Person {
final String name;
// ···
operator ==(other) => other is Person && name == other.name;
int get hashCode => name.hashCode;
}
~~~
~~~
【bad】
class Person {
final String name;
// ···
operator ==(other) => other != null && ...
}
~~~
';
参数指南
最后更新于:2022-04-02 05:55:29
[TOC]
在Dart中,可选参数可以是位置参数,也可以是命名参数,但不能同时参考。
## 避免位置布尔参数。
与其他类型不同,布尔值通常以文字形式使用。像数字这样的东西通常被包装在命名的常量中,但是我们通常直接传递真和假。如果不清楚布尔值表示什么,就会导致callsites无法读取:
~~~
【bad】
new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);
~~~
相反,考虑使用命名参数、命名构造函数或命名常量来阐明调用的作用。
~~~
Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);
~~~
注意,这并不适用于setter,在setter中名称清楚地表示值的含义:
~~~
listBox.canScroll = true;
button.isEnabled = false;
~~~
## 如果用户可能想要省略早期的参数,则避免可选的位置参数。
可选的位置参数应该具有逻辑级别,使得前面的参数比后面的参数更频繁地被传递。 用户几乎不需要显式传递“占位”来省略先前的位置参数以便后边参数的传递。 最好还是使用命名参数。
~~~
String.fromCharCodes(Iterable charCodes, [int start = 0, int end]);
DateTime(int year,
[int month = 1,
int day = 1,
int hour = 0,
int minute = 0,
int second = 0,
int millisecond = 0,
int microsecond = 0]);
Duration(
{int days = 0,
int hours = 0,
int minutes = 0,
int seconds = 0,
int milliseconds = 0,
int microseconds = 0});
~~~
## 避免强制参数接受一个特殊的“无参数”值。
如果用户在逻辑上漏掉了一个参数,那么宁愿让他们通过使参数可选而实际上忽略它,而不是强迫他们传递null、空字符串或其他表示“未传递”的特殊值。
省略参数更简洁,有助于防止错误,当用户认为标记值(如null)提供了真正的值时,意外地传递了该值。
~~~
var rest = string.substring(start);
~~~
~~~
【bad】
var rest = string.substring(start, null);
~~~
## 请使用包含开始和独占结束参数来接受范围。
如果您正在定义一个方法或函数,该方法或函数允许用户从整数索引序列中选择元素或项的范围,那么可以使用start索引,它指的是第一个项和一个(可能是可选的)比最后一个项的索引大一个的结束索引。
这与做同样事情的核心库是一致的。
~~~
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
~~~
在这里保持一致特别重要,因为这些参数通常是未命名的。如果您的API使用的是长度而不是端点,那么在callsite上根本就看不到差异。
';
类型设计
最后更新于:2022-04-02 05:55:27
[TOC]
在程序中记下类型时,会限制流入代码不同部分的值的类型。类型可以出现在两种地方:声明上的类型注释和泛型调用的类型参数。
当您想到“静态类型”时,类型注释是您通常会想到的。您可以键入注释变量,参数,字段或返回类型。在以下示例中,bool和String为类型注释。它们挂起代码的静态声明结构,并且不会在运行时“执行”。
~~~
bool isEmpty(String parameter) {
bool result = parameter.length == 0;
return result;
}
~~~
泛型调用是集合字面量、对泛型类构造函数的调用或泛型方法的调用。在下一个示例中,num和int是泛型调用的类型参数。尽管它们是类型,但它们是一级实体,在运行时被具体化并传递给调用。
~~~
var lists = [1, 2];
lists.addAll(List.filled(3, 4));
lists.cast();
~~~
我们在这里强调“泛型调用”部分,因为类型参数也可以 出现在类型注释中:
~~~
List ints = [1, 2];
~~~
在这里,int是一个类型参数,但是它出现在类型注释中,而不是泛型调用中。您通常不需要担心这种区别,但是在一些地方,对于在通用调用中使用类型而不是类型注释,我们有不同的指导教程。
在大多数情况下,Dart允许您省略类型注释,并根据附近的上下文为您推断类型,或者默认为动态类型。Dart同时具有类型推断和动态类型的事实导致了人们对代码是“非类型”的含义的困惑。这是否意味着代码是动态类型的,或者您没有编写类型?为了避免混淆,我们避免说“untyping”,而是使用以下术语:
* 如果代码是带类型注释的,则该类型显式地写在代码中。
* 如果推断出代码,则没有编写类型注释,Dart自己成功地找到了类型。推理可能会失败,在这种情况下,指南不考虑推断。在某些地方,推理失败是静态错误。在其他情况下,Dart使用了动态备份类型。
* 如果代码是动态的,那么它的静态类型就是特殊的动态类型。代码可以被显式地注释为动态的,也可以被推断出来。
换句话说,某些代码是注释的还是推断的,与它是动态的还是其他类型的正交。
推理是一种强大的工具,可以让您省去编写和阅读那些明显或无趣的类型的工作。在明显的情况下省略类型也会将读者的注意力吸引到显式类型上,因为这些类型很重要,比如强制类型转换。
显式类型也是健壮,可维护代码的关键部分。它们定义了API的静态形状。它们记录并强制允许哪些值允许到达程序的不同部分。
这里的指导方针在我们在简洁性和明确性,灵活性和安全性之间找到了最佳平衡。在决定要编写哪些类型时,您需要回答两个问题:
* 我应该写哪种类型,因为我认为最好让它们在代码中可见?
* 我应该写哪种类型因为推理无法为我提供这些类型?
这些指南可以帮助您回答第一个问题:
* 如果类型不明显,则优先对公共字段和顶级变量进行类型注释。
* 如果类型不明显,请考虑对私有字段和顶级变量进行类型注释。
* 避免类型注释初始化的局部变量。
* 避免在函数表达式上注释推断的参数类型。
* 避免泛型调用上的冗余类型参数。
这些涵盖了第二个:
* 当Dart推断出错误的类型时,请进行注释。
* 优先使用动态注释,而不是让推断失败。
其余指南涵盖了有关类型的其他更具体的问题。
## 如果类型不明显,则优先对公共字段和顶级变量进行类型注释。
类型注释是关于如何使用库的重要文档。它们在程序的区域之间形成边界以隔离类型错误的来源。考虑:
~~~
【bad】
install(id, destination) => ...
~~~
这里,id是什么还不清楚。一个字符串?目的是什么?字符串还是文件对象?这个方法是同步的还是异步的?这是清晰的:
~~~
Future install(PackageId id, String destination) => ...
~~~
但在某些情况下,类型是如此明显,以至于编写它是毫无意义的:
~~~
const screenWidth = 640; // Inferred as int.
~~~
“显而易见”并没有明确的定义,但这些都是很好的候选者:
* 字面量。
* 构造函数调用。
* 对显式类型化的其他常量的引用。
* 数字和字符串的简单表达式。
* 工厂方法,如int.parse()、Future.wait()等,读者应该很熟悉。
如果有疑问,请添加类型注释。即使类型很明显,您可能仍然希望显式注释。如果推断类型依赖于来自其他库的值或声明,您可能希望键入注释您的声明,以便对其他库的更改不会在您没有意识到的情况下悄无声息地更改您自己的API的类型。
## 如果类型不明显,请考虑对私有字段和顶级变量进行类型注释。
在公开声明上键入注释可以帮助代码的用户。私有成员上的类型帮助维护人员。私有声明的范围更小,那些需要知道声明类型的人也更可能熟悉周围的代码。这使得更依赖于推理和省略私有声明类型变得合理,这就是为什么这个指南比上一个指南更温和的原因。
如果您认为初始化器表达式(无论它是什么)足够清晰,那么您可以省略注释。但是如果您认为注释有助于使代码更清晰,那么添加一个注释。
## 避免类型注释初始化的局部变量。
局部变量的作用域非常小,尤其是在函数往往很小的现代代码中。省略类型会将读者的注意力集中在变量的更重要的名称及其初始值上。
~~~
List
';
- > possibleDesserts(Set
- >[];
for (var recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
~~~
~~~
【bad】
List
- > possibleDesserts(Set
- > desserts =
- >[];
for (List
成员设计
最后更新于:2022-04-02 05:55:25
[TOC]
成员属于对象,可以是方法变量也可以是实例变量。
## 优先使用final字段和顶级变量。
不可变的状态 - 随着时间的推移不会改变 - 程序员更容易推理。最小化它们使用的可变状态量的类和库往往更容易维护。
当然,拥有可变数据通常很有用。但是,如果您不需要它,您的默认值应该是尽可能地创建字段和顶级变量为final。
## 请使用getter进行概念上访问属性的操作。
决定一个成员什么时候应该是getter,什么时候应该是方法,这是一个具有挑战性的、微妙的、但却是好的API设计的重要部分,因此这是一个很长的指导原则。其他一些语言的文化则回避取值器。他们只在操作与实际操作几乎完全相同的情况下才使用它们——它对完全依赖于对象的状态进行了极少量的计算。任何比这更复杂或更重量级的东西都会在名称后面跟随(),以表示“在这里需要计算!”因为a.后面有一个名称。意思是“字段”。
Dart不是那样的。在Dart中,所有带点的名称都是可能进行计算的成员调用。字段是特殊的——它们是由语言提供实现的getter。换句话说,在Dart中,getter不是“特别慢的字段”;字段是“特别快速的getter”。
即便如此,选择getter而不是方法会向调用者发送重要信号。粗略地说,信号是这个操作是“字段一样的”。就调用者所知,该操作至少在原则上可以使用字段实现。这意味着:
即便如此,选择方法上的getter会向调用者发送一个重要信号。大致的信号是操作是“像字段一样”。至少在原理上,操作可以使用字段来实现,只要调用者者知道。这意味着:
* 该操作不接受任何参数并返回结果。
* 调用者主要关心结果。如果你想调用者担心如何操作产生的结果比他们多一点,结果被生产,然后给操作描述工作一个动词的名字,使之方法。
但这并不意味着该操作必须是在为了一个getter特别快。IterableBase.length是O(n),那没关系。对于取值器进行重要计算是好的。但是如果它做了大量的工作,你可能需要通过使它成为一个描述其功能的动词的方法来引起他们的注意。
~~~
【bad】
connection.nextIncomingMessage; // Does network I/O.
expression.normalForm; // Could be exponential to calculate.
~~~
* 该操作没有用户可见的副作用。访问实际字段不会改变程序中的对象或任何其他状态。它不会产生输出,写入文件等。一个getter也不应该做那些事情。
“用户可见”部分很重要。取值器修改隐藏状态或产生带外副作用是很好的。Getters可以懒惰地计算并存储他们的结果,写入缓存,记录东西等。只要调用者不关心副作用,它可能就好了。
~~~
【bad】
stdout.newline; // Produces output.
list.clear; // Modifies object.
~~~
* 该操作是幂等的。“幂等”是一个奇怪的词,在这种情况下,基本上意味着每次调用操作多次会产生相同的结果,除非在这些调用之间明确修改某些状态。(显然,list.length如果在调用之间向列表中添加元素,则会产生不同的结果。)
这里的“相同结果”并不意味着getter必须在连续调用中逐字地生成相同的对象。要求这将迫使许多吸气者进行脆弱的缓存,这否定了使用取值器的全部意义。每次调用它时,吸气剂返回一个新的未来或列表是很常见的,而且非常好。重要的是,未来完成相同的值,列表包含相同的元素。
换句话说,结果值应该与调用者关心的方面相同。
~~~
【bad】
DateTime.now; // New result each time.
~~~
* 生成的对象不会公开所有原始对象的状态。 字段仅显示一个对象。如果您的操作返回一个公开原始对象的整个状态的结果,那么它最好作为一个to___()或一个as___()方法。
如果以上所有描述了您的操作,它应该是一个取值器。似乎很少有成员会幸免于难,但令人惊讶的是很多成员。许多操作只是对某些状态进行一些计算,其中大多数可以而且应该是getter。
~~~
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;
~~~
## 请将setter用于从概念上改变属性的操作。
在setter与方法之间做出决定类似于在getter与方法之间做出决定。在这两种情况下,操作应该是“类似字段”。
对于setter,“类似字段”意味着:
* 该操作采用单个参数,不会产生结果值。
* 该操作会更改对象中的某些状态。
* 该操作是幂等的。对于相同的值,使用相同的值调用相同的setter两次,就调用者而言,第二次不应该执行任何操作。在内部,也许你有一些缓存失效或记录正在进行。没关系。但从取值器的角度来看,似乎第二次取值什么也没做。
~~~
rectangle.width = 3;
button.visible = false;
~~~
## 不要在没有相应取值器的情况下定义设置器。
用户将getter和setter视为对象的可见属性。可以写入但未被看到的“dropbox”属性令人困惑,并且混淆了他们对属性如何工作的直觉。例如,没有getter的setter意味着你可以=用来修改它,但不是+=。
本指南也并不意味着你应该添加一个getter只是允许您要添加的设置器。对象通常不应该暴露出比他们需要更多的状态。如果某个对象的某个状态可以修改但不能以相同的方式公开,请改用方法。
> 这条规则有一个例外。一个 Angular组件类可以暴露是从模板调用以初始化该组件设置器。通常,这些setter不是要从Dart代码调用的,也不需要相应的getter。(如果从Dart代码中使用它们,它们应该有一个吸气剂。)
>
## 避免返回null从成员返回类型为bool,double,int,或num。
即使Dart中所有类型都可以为空,但用户认为这些类型几乎从不包含null,而小写名称则鼓励“Java原始”思维模式。
在API中有一个“可以为空的原语”类型可能偶尔会有用,例如,表示地图中某些键没有值,但这些应该很少见。
如果您确实有可能返回的此类成员null,请将其记录得非常清楚,包括null将返回的条件。
## 避免只是为了启用流畅的接口从方法中返回this值。
方法级联是链接方法调用的更好解决方案。
~~~
var buffer = StringBuffer()
..write('one')
..write('two')
..write('three');
~~~
以下是反例:
~~~
var buffer = StringBuffer()
.write('one')
.write('two')
.write('three');
~~~
';
构造函数
最后更新于:2022-04-02 05:55:23
[TOC]
通过声明与类具有相同名称的函数以及可选的附加标识符来创建Dart构造函数。后者称为命名构造函数。
## 优先定义构造函数而不是静态方法来创建实例。
构造函数使用new或调用const,它传达调用的主要目的是返回类的实例(或至少是实现其接口的东西)。
您永远不需要使用静态方法来创建实例。命名构造函数允许您阐明如何创建对象,工厂构造函数允许您在适当时构造子类或子接口的实例。
尽管如此,一些技术上创建新对象的方法并不像“类似构造函数”。例如,Uri.parse()即使它从给定的参数创建一个新URI ,也是一个静态方法。同样,使用静态方法可以更好地读取实现Builder模式的类。
但是,在大多数情况下,你应该使用构造函数,即使它更冗长。当用户想要一个新的类实例时,他们希望构造函数是创建一个实例的常规方法。
~~~
class Point {
num x, y;
Point(this.x, this.y);
Point.polar(num theta, num radius)
: x = radius * cos(theta),
y = radius * sin(theta);
}
~~~
以下是反面例子:
~~~
class Point {
num x, y;
Point(this.x, this.y);
static Point polar(num theta, num radius) =>
Point(radius * cos(theta), radius * sin(theta));
}
~~~
## const如果类支持它,请考虑构造函数。
如果你有一个所有字段都是final的类,并且构造函数除了初始化它们之外什么都不做,你可以创建那个构造函数const。这允许用户在需要常量的位置创建类的实例 -- 在其他更大的常量,切换案例,默认参数值等内部。
如果你没有明确地做到const,他们就无法做到。
但请注意,const构造函数是您的公共API中的承诺。如果稍后将构造函数更改为非构造函数const,则会破坏在常量表达式中调用它的用户。如果您不想承诺,请不要这样做const。实际上,const构造函数对于简单的,不可变的数据记录类很有用。
';
类的设计
最后更新于:2022-04-02 05:55:20
[TOC]
Dart是一种“纯粹的”面向对象语言,因为所有对象都是类的实例。但Dart并不要求在类中定义所有代码 - 您可以在过程或函数语言中定义顶级变量,常量和函数。
## 避免在简单函数可以胜任时定义一个成员的抽象类。
与Java不同,Dart具有一流的功能,闭包和使用它们的轻松语法。如果您只需要回调,只需使用一个函数即可。如果您正在定义一个类,并且它只有一个具有无意义名称的抽象成员,call或者invoke,您很可能只需要一个函数。
~~~
typedef Predicate = bool Function(E element);
~~~
以下是反面例子:
~~~
abstract class Predicate {
bool test(E element);
}
~~~
## 避免定义仅包含静态成员的类。
在Java和C#中,每个定义都必须在一个类中,所以通常看到“类”只存在于静态成员的位置。其他类用作命名空间 - 一种为一堆成员提供共享前缀以使它们彼此关联或避免名称冲突的方法。
Dart具有顶级函数,变量和常量,因此您不需要一个类来定义某些东西。如果您想要的是命名空间,那么库更适合。库支持导入前缀和显示/隐藏(show/hide)组合器。这些功能强大的工具可让代码的使用者以最适合他们的方式处理名称冲突。
如果函数或变量在逻辑上与类无关,请将其置于顶层。如果您担心名称冲突,请为其指定更精确的名称,或将其移动到可以使用前缀导入的单独库中。
~~~
DateTime mostRecent(List dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
const _favoriteMammal = 'weasel';
~~~
以下是反面例子:
~~~
class DateUtils {
static DateTime mostRecent(List dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
}
class _Favorites {
static const mammal = 'weasel';
}
~~~
在惯用的Dart中,类定义了各种对象。从未实例化的类型是代码特征。
但是,这不是一个严格的规定。对于常量和类似枚举的类型,将它们组合在一个类中可能是很自然的。
~~~
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
~~~
## 避免扩展了一个不打算进行子类扩展的类。
如果构造函数从生成构造函数更改为工厂构造函数,则调用该构造函数的任何子类构造函数都将中断。此外,如果一个类更改了它调用的自己的方法this,那么可能会破坏覆盖这些方法的子类,并期望在某些点调用它们。
这两个都意味着一个类需要考虑是否要允许子类化。这可以在doc注释中提现,或者通过给类明显的名称来表达IterableBase。如果类的作者不这样做,它的最好假设你应该不扩展类。否则,稍后更改它可能会破坏您的代码。
## 如果你的类支持扩展请提供文档注释
这是上述规则的必然结果。如果要允许类的子类,请说明。使用类名称后缀Base,或在类的doc注释中提及它。
## 避免实现一个不打算成为接口的类。
隐式接口是Dart中的一个强大工具,可以避免在从该合同的实现签名中轻易推断出类的合同时重复它。
但实现类的接口是与该类非常紧密的耦合。它实际上意味着对您正在实现的接口的类的任何更改都将破坏您的实现。例如,向类中添加新成员通常是安全,不间断的更改。但是如果你正在实现该类的接口,那么现在你的类有一个静态错误,因为它缺少该新方法的实现。
库维护者需要能够在不破坏用户的情况下发展现有类。如果你把每个类都视为暴露了一个用户可以自由实现的接口,那么改变这些类就变得非常困难了。反过来,这种困难意味着您所依赖的库增长速度较慢,能够适应新的需求。
为了给你的类的作者提供更多的余地,避免实现隐式接口,除了明确要实现的类。否则,您可能会引入作者不想要的耦合,并且可能会在没有意识到的情况下破坏您的代码。
## 如果你的类支持作为接口使用请提供文档注释。
如果您的类可以用作接口,请在类的doc注释中提及。
## 避免在并不支持mixing的类中使用mixin
如果将一个构造函数添加到以前没有定义任何类的类中,那么会破坏混合它的任何其他类。这是类中看似无害的变化,并且对mixin的限制并不广为人知。作者可能会添加一个构造函数而不会意识到它会破坏你的类混合它。
与子类化一样,这意味着类需要考虑是否允许将其用作mixin。如果班级没有文档注释或明显的名称IterableMixin,你应该假设你不能在类中使用mix。
## 如果您的类支持用作mixin,请提供文档注释。
在类的文档注释中提及该类是否可以或必须用作mixin。如果您的类只是作为mixin使用,那么请考虑添加 Mixin到类名的末尾。
';
库的设计
最后更新于:2022-04-02 05:55:18
[TOC]
前导下划线字符(_)表示成员对其库是私有的。这不仅仅是惯例,而是内置于语言本身。
## 首选将声明设为私有。
库中的公共声明--顶级或类--是其他库可以并且应该访问该成员的信号。它也是您库的一项承诺,即支持它并在其发生时正常行事。
如果那不是你想要的,那就加下换线(_)。更少暴露的公共接口更易于维护,用户可以更轻松地学习。分析器也将告诉您未使用的私有声明,以便您可以删除冗余代码。如果成员是公共的,则不能这样做,因为它不知道其视图之外的任何代码是否正在使用它。
## 考虑在同一个库中声明多个类。
某些语言(如Java)将文件组织与类组织联系起来 --每个文件只能定义一个顶级类。Dart没有这个限制。库是与类分开的不同实体。单个库包含多个类,顶级变量和函数(如果它们在逻辑上都属于一起)是完全没问题的。
在一个库中放置多个类可以启用一些有用的模式。由于Dart中的隐私在库级而不是类级别工作,因此这是一种在C ++中定义“friend”类的方法。在同一个库中声明的每个类都可以访问彼此的私有成员,但该库外的代码不能访问。
当然,本指南并不意味着您应该将所有类放入一个巨大的整体库中,只是允许您在一个库中放置多个类。
';
命名
最后更新于:2022-04-02 05:55:16
[TOC]
命名是编写可读,可维护代码的重要部分。以下最佳实践可帮助您实现该目标。
## 请务必使用条款。
在整个代码中,对同一个名称使用相同的名称。如果用户可能知道的API之外已存在先例,请遵循该先例。
~~~
pageCount //一个字段。
updatePageCount ()//与pageCount一致。
toSomething ()//与Iterable的toList()一致。
asSomething ()//与List的asMap()一致。
Point //一个熟悉的概念。
~~~
以下是应该避免的示例:
~~~
renumberPages () //与pageCount混淆不同。
convertToSomething () //与toX()先例不一致。
wrappedAsSomething () //与asX()先例不一致。
Cartesian //大多数用户都不熟悉。
~~~
目标是利用用户已经知道的东西。这包括他们对问题域本身的知识,核心库的约定以及您自己的API的其他部分。通过构建这些知识,您可以减少他们在获得高效率之前必须获得的新知识量。
## 避免缩写。
除非缩写比未缩写的术语更常见,否则不要缩写。如果您缩写,请正确地将其大写。
~~~
pageCount
buildRectangles
IOStream
HttpRequest
~~~
以下是应该避免的示例:
~~~
numPages //“num”是number(of)的缩写
buildRects
InputOutputStream
HypertextTransferProtocolRequest
~~~
## 优先将最具描述性的名词放在最后。
最后一个词应该是最具描述性的东西。您可以在其前面添加其他单词,例如形容词,以进一步描述该事物。
~~~
pageCount //(页数)计数。
ConversionSink //用于进行转换的接收器。
ChunkedConversionSink //一个被分块的ConversionSink。
CssFontFaceRule // CSS中字体面的规则。
~~~
以下是应该避免的示例:
~~~
numPages //不是页面的集合。
CanvasRenderingContext2D //不是“2D”。
RuleFontFaceCss //不是CSS。
~~~
## 考虑将代码读成句子。
如果对命名有疑问,请使用你的API写一些代码,然后尝试像读普通语句一样阅读他们。
~~~
// "If errors is empty..."
if (errors.isEmpty) ...
// "Hey, subscription, cancel!"
subscription.cancel();
// "Get the monsters where the monster has claws."
monsters.where((monster) => monster.hasClaws);
~~~
以下是存在问题的示例:
~~~
// Telling errors to empty itself, or asking if it is?
if (errors.empty) ...
// Toggle what? To what?
subscription.toggle();
// Filter the monsters with claws *out* or include *only* those?
monsters.filter((monster) => monster.hasClaws);
~~~
尝试使用API并查看在代码中使用它时如何“阅读”会很有帮助,但是你可能会走得太远。这不是有益的补充文章和讲话的其他部分,迫使你的名字从字面上看像一个语法正确的句子。例如:
~~~
if (theCollectionOfErrors.isEmpty) ...
monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);
~~~
## 首选非布尔属性或变量的名词短语。
读者关注的是房产是什么。如果用户更关心 如何确定属性,那么它应该是一个带有动词短语名称的方法。
~~~
list.length
context.lineWidth
quest.rampagingSwampBeast
~~~
以下是反面示例:
~~~
list.deleteItems
~~~
## 首选布尔属性或变量的非命令性动词短语。
布尔名称通常用作控制流中的条件,因此您需要一个在那里读取的名称。比较:
~~~
if (window.closeable) ... // Adjective.
if (window.canClose) ... // Verb.
~~~
好名字往往以几种动词中的一种开头:
* 对于一个form表单来说: ,isEnabled,。wasShown willFire到目前为止,这些是最常见的。
* 一个辅助动词:hasElements,canClose, shouldConsume,mustSave。
* 一个主动词:ignoresInput,wroteFile。这些很少见,因为它们通常含糊不清。loggedResult是一个坏名字,因为它可能意味着“无论是否记录结果”或“记录的结果”。同样,closingConnection可以是“连接是否正在关闭”或“正在关闭的连接”。当名称只能作为谓词读取时,允许使用主动词。
将所有这些动词短语与方法名称区分开来的是它们不是必要的。 布尔名称永远不应该像告诉对象做某事的命令,因为访问属性不会更改对象。 (如果属性确实以有意义的方式修改了对象,那么它应该是一个方法。)
~~~
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
~~~
反面例子:
~~~
empty // Adjective or verb?
withElements // Sounds like it might hold elements.
closeable // Sounds like an interface.
// "canClose" reads better as a sentence.
closingWindow // Returns a bool or a window?
showPopup // Sounds like it shows the popup.
~~~
>这条规则有一个例外。Angular 组件中的输入属性有时会使用命令式动词来表示布尔设置器,因为这些setter是在模板中调用的,而不是从其他Dart代码中调用的。
## 考虑省略命名布尔参数的动词。
这改进了先前的规则。对于布尔值的命名参数,名称通常在没有动词的情况下一样清晰,并且代码在调用站点处读取更好。
~~~
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);
~~~
## 首选布尔属性或变量的“正”名称。
大多数布尔名称具有概念上的“正面”和“负面”形式,前者感觉就像基本概念,后者是否定 - “开放”和“封闭”,“启用”和“禁用”等。通常后者的名称从字面上有否定前者的前缀:“可见”和“ 不可见”,“已连接”和“ 连接已关闭”,“零”和“ 非 零”。
当选择true代表两种情况中的哪一种 - 以及属性的名称属于哪种情况时 - 更喜欢积极或更基本的情况。布尔成员通常嵌套在逻辑表达式中,包括否定运算符。如果你的属性本身就像一个否定,那么读者就更难以在心理上执行双重否定并理解代码的含义。
~~~
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}
~~~
以下是反面例子:
~~~
if (!socket.isDisconnected && !database.isEmpty) {
socket.write(database.read());
}
~~~
此规则的一个例外是负面形式是用户绝大多数需要使用的属性。选择正向的情况将迫使他们在任何地方使用感叹号(!)对属性取反。相反,对该属性使用否定案例可能更好。
对于某些属性,没有明显的正面形式。已刷新到磁盘的文档是“已保存”或“未更改”? 文档是否未刷新“ 未保存”或“已更改”?在模棱两可的情况下,倾向于不太可能被用户否定或具有较短名称的选择。
## 首选一个主要目的是副作用的函数或方法的命令式动词短语。
可调用成员可以将结果返回给调用者并执行其他工作或副作用。在像Dart这样的命令式语言中,成员通常主要是因为他们的副作用:他们可能会改变对象的内部状态,产生一些输出或与外界交谈。
这些类型的成员应该使用命令式动词短语来命名,该短语阐明了成员所执行的工作。
~~~
list.add("element");
queue.removeFirst();
window.refresh();
~~~
这样,调用就像执行该工作的命令一样。
## 如果返回值是其主要目的,则首选函数或方法的名词短语或非命令性动词短语。
其他可调用成员几乎没有副作用,但会向调用者返回有用的结果。如果成员不需要参数来执行此操作,则通常应该是getter。但是,有时逻辑“属性”需要一些参数。例如, elementAt()从集合中返回一段数据,但它需要一个参数来知道要返回哪一块数据。
这意味着该成员在语法上是一个方法,但从概念上讲它是一个属性,并且应该使用描述成员返回内容的短语来命名。
~~~
var element = list.elementAt(3);
var first = list.firstWhere(test);
var char = string.codeUnitAt(4);
~~~
本指南比前一个指令影响小。有时,一个方法没有副作用,但仍然是简单的名称与动词短语像 list.take()或string.split()。
## 如果要引起对其执行的工作的注意,请考虑函数或方法的命令式动词短语。
当一个成员产生没有任何副作用的结果时,它通常应该是一个getter或一个名词短语名称描述它返回的结果的方法。但是,有时产生该结果所需的工作很重要。它可能容易出现运行时故障,或者使用网络或文件I/O等重量级资源。在这种情况下,您希望调用者考虑成员正在进行的工作,为成员提供描述该工作的动词短语名称。
~~~
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();
~~~
但请注意,此指南比前两个更柔和。操作执行的工作通常是与调用者无关的实现细节,并且性能和健壮性边界随时间而变化。在大多数情况下,成员的命名基于他们为调用者做了什么,而不是他们为调用者怎么做。
## 避免用get为前缀来命名方法名。
在大多数情况下,该方法应该是除去get的成员的取值器 。例如,名为getBreakfastOrder()的方法,定义一个名为的 breakfastOrder的取值器。
即使成员确实需要成为一个方法,因为它需要参数或者getter不能满足要求,你仍然应该避免get。与之前的指南一样,要么:
* 简单地删除get并使用名词短语名称例如breakfastOrder() 如果调用者主要关心方法返回的值。
* 使用动词短语的名字,如果调用者关心的工作正在做,但需要挑选一个比get更精确地描述的动词,如 create,download,fetch,calculate,request,aggregate,等。
## 优先使用to__()命名方法,如果这个方法拷贝一个对象的状态到另一个状态。
转换方法是返回一个新对象的方法,该对象包含几乎所有接收器状态的副本,但通常采用某种不同的形式或表示形式。 核心库有一个约定,即这些方法的名称首先跟随结果类型。
如果您定义转换方法,遵循该约定会很有帮助。
~~~
list.toSet();
stackTrace.toString();
dateTime.toLocal();
~~~
## 优先使用as__()命名方法,如果他返回由原始对象支持的不同表示。
转换方法是“快照”。 生成的对象具有自己的原始对象状态的副本。 还有其他类似转换的方法返回视图 - 它们提供了一个新对象,但该对象引用了原始对象。 稍后对原始对象的更改将反映在视图中。
您要遵循的核心库约定是as___()。
~~~
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();
~~~
## 避免描述函数或方法名称中的参数。
用户将在调用点看到参数,因此通常无法在名称本身中引用它。
~~~
list.add(element);
map.remove(key);
~~~
以下是反面例子:
~~~
list.addElement(element)
map.removeKey(key)
~~~
但是,提及一个参数可以将其与其他类似命名的采用不同类型的方法消除歧义:
~~~
map.containsKey(key);
map.containsValue(value);
~~~
## 在命名类型参数时,请遵循现有的助记符约定。
单字母名称并不完全有启发性,但几乎所有通用类型都使用它们。幸运的是,他们大多以一致的助记方式使用它们。惯例是:
*E对于集合中的元素类型:
~~~
class IterableBase {}
class List {}
class HashSet {}
class RedBlackTree {}
~~~
* K以及关联集合中V的键和值类型:
~~~
class Map {}
class Multimap {}
class MapEntry {}
~~~
* R对于用作函数的返回类型或类的方法的类型。这种情况并不常见,但有时会出现在typedef中,也会出现在实现访问者模式的类中:
~~~
abstract class ExpressionVisitor {
R visitBinary(BinaryExpression node);
R visitLiteral(LiteralExpression node);
R visitUnary(UnaryExpression node);
}
~~~
* 否则,将T,S和U用于具有单个类型参数的泛型,并且周围类型使其含义明显。 这里有多个字母允许嵌套而不会遮蔽周围的名称。 例如:
~~~
class Future {
Future then(FutureOr onValue(T value)) => ...
}
~~~
这里,泛型方法then\()用于S避免覆盖T 在Future\。
## 如果上述情况都不合适,则可以使用另一个单字母助记符名称或描述性名称:
~~~
class Graph {
final List nodes = [];
final List edges = [];
}
class Graph {
final List nodes = [];
final List edges = [];
}
~~~
实际上,现有的约定涵盖了大多数类型参数。
';
设计实践
最后更新于:2022-04-02 05:55:14
下面是一些为库编写一致、可用的api的指南。
';
异步
最后更新于:2022-04-02 05:55:11
[TOC]
Dart有几个语言特性来支持异步编程。以下最佳实践适用于异步编码。
## 优先使用async/await代替原始的futures
异步代码是出了名的难以阅读和调试,即使是在使用像futures这样的抽象时也是如此。async/await语法提高了可读性,允许您在异步代码中使用所有Dart控制流结构。
以下是推荐的示例:
~~~
Future countActivePlayers(String teamName) async {
try {
var team = await downloadTeam(teamName);
if (team == null) return 0;
var players = await team.roster;
return players.where((player) => player.isActive).length;
} catch (e) {
log.error(e);
return 0;
}
}
~~~
以下是不推荐的示例:
~~~
Future countActivePlayers(String teamName) {
return downloadTeam(teamName).then((team) {
if (team == null) return Future.value(0);
return team.roster.then((players) {
return players.where((player) => player.isActive).length;
});
}).catchError((e) {
log.error(e);
return 0;
});
}
~~~
## 当异步没有任何用处时,不要使用它。
很容易养成在任何与异步相关的函数上使用异步的习惯。但在某些情况下,它是无关的。如果可以在不改变函数行为的情况下省略异步,那么就这样做。
以下是推荐示例:
~~~
Future afterTwoThings(Future first, Future second) {
return Future.wait([first, second]);
}
~~~
以下是不推荐的示例:
~~~
Future afterTwoThings(Future first, Future second) async {
return Future.wait([first, second]);
}
~~~
使用异步的情况包括:
* 您使用的是await。(这是显而易见的。)
* 您正在异步返回一个错误。异步然后抛出比返回Future.error(…)短。
* 您正在返回一个值,并且希望它隐式地包装在future。异步比Future.value(…)短。
~~~
Future usesAwait(Future later) async {
print(await later);
}
Future asyncError() async {
throw 'Error!';
}
Future asyncValue() async => 'value';
~~~
## 考虑使用高阶方法转换流。
这与上面关于迭代的建议相似。Streams支持许多相同的方法,并且正确地处理传输错误、关闭等问题。
## 避免直接使用Completer。
许多不熟悉异步编程的人都希望编写能够创造future的代码。Future的构造函数似乎不适合它们的需要,所以它们最终找到Completer类并使用它。
以下是错误示例:
~~~
Future fileContainsBear(String path) {
var completer = Completer();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}
~~~
有两种底层代码需要Completer:新的异步原语和不使futures的异步代码接口。大多数其他代码应该使用async/await或Future.then(),因为它们更清晰,并且让错误处理更容易。
~~~
Future fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
~~~
~~~
Future fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains('bear');
}
~~~
## 在消除 FutureOr\(其类型参数可以是对象)的歧义时,对Future\进行测试。
在使用FutureOr\执行任何有用的操作之前,通常需要检查是否有Future\或明确的T。如果type参数是某个特定类型,如FutureOr\ ,使用哪个测试无关紧要,是int还是Future\。 两者都有效,因为这两种类型是不相交的。
但是,如果值类型是Object或可能使用Object实例化的类型参数,则两个分支重叠。 Future\
';
错误处理
最后更新于:2022-04-02 05:55:09
[TOC]
当程序中出现错误时,Dart会使用异常。以下最佳实践适用于捕获和抛出异常。
## 避免无on子句的捕获。
没有on限定符的catch子句捕获try块中代码抛出的任何内容。Pokemon异常处理很可能不是你想要的。您的代码是否正确处理StackOverflowError或OutOfMemoryError?如果您错误地将错误的参数传递给该try块中的方法,您是否希望让调试器指向您的错误,或者您是否愿意接受有用的ArgumentError?你是否希望代码中的任何assert()语句有效地消失,因为你正在捕获抛出的AssertionErrors?
答案可能是“不”,在这种情况下,您应该过滤掉捕获的类型。在大多数情况下,您应该有一个on子句,它限制您了解并且正确处理的运行时故障类型。
在极少数情况下,您可能希望捕获任何运行时错误。这通常是在框架或低级代码中,它试图隔离任意应用程序代码而不会导致问题。即使在这里,捕获异常通常比捕获所有类型更好。异常是所有运行时错误的基类,并排除了指示代码中程序错误的错误。
## 不要在没有on子句的情况下丢弃捕获的错误。
如果您确实觉得需要捕获从代码区域中抛出的所有内容,那么就用捕获的内容做一些事情。记录它,显示它给用户或重新扔它,但不要默默地丢弃它。
## 抛出只针对编程错误实现Error的对象。
Error类是程序错误的基类。当抛出该类型的对象或其子接口之一ArgumentError时,这意味着您的代码中存在一个bug。当您的API想要向调用者报告它正在被错误地使用时,抛出一个错误会清楚地发送该信号。
相反,如果异常是某种运行时失败,并不表示代码中有错误,那么抛出错误就是误导。相反,抛出一个核心异常类或其他类型。
## 不要显式地捕捉Error或实现Error的类型。
从上面可以看出。由于错误表明代码中有错误,它应该展开整个callstack,停止程序,并打印堆栈跟踪,这样您就可以定位和修复错误。
捕捉这些类型的错误会破坏这个过程并掩盖错误。与其在事后添加错误处理代码来处理这个异常,不如返回并修复导致它被抛出的代码。
## 一定要使用rethrow来重新抛出一个捕获的异常。
如果决定重新抛出异常,最好使用rethrow语句,而不是使用throw抛出相同的异常对象。rethrow保留了异常的原始堆栈跟踪。另一方面,throw重置堆栈跟踪到最后抛出的位置。
以下是错误示例:
~~~
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
~~~
以下是正确示例:
~~~
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
~~~
';
构造函数
最后更新于:2022-04-02 05:55:07
[TOC]
下面的最佳实践适用于为类声明构造函数。
## 尽可能使用初始化的形式。
许多字段直接从构造函数参数初始化,如:
以下是不推荐的写法:
~~~
class Point {
num x, y;
Point(num x, num y) {
this.x = x;
this.y = y;
}
}
~~~
以下是推荐的写法:
~~~
class Point {
num x, y;
Point(this.x, this.y);
}
~~~
构造函数参数前的this.语法称为“初始化形式”。你不能总是利用它。特别是,使用它意味着参数在初始化列表中不可见。但是,当你需要这样做时,你应该这样做。
## 不要键入注释初始化的形式。
如果构造函数参数使用this.初始化一个字段,那么该参数的类型被理解为与字段的类型相同。
以下是推荐的写法:
~~~
class Point {
int x, y;
Point(this.x, this.y);
}
~~~
不要使用以下写法:
~~~
class Point {
int x, y;
Point(int this.x, int this.y);
}
~~~
## 对于空的构造函数体使用;而不是{}。
在Dart中,带有空主体的构造函数只能以分号结束。(事实上,const构造函数需要它。)
以下是推荐的写法:
~~~
class Point {
int x, y;
Point(this.x, this.y);
}
~~~
以下是不推荐的写法:
~~~
class Point {
int x, y;
Point(this.x, this.y) {}
}
~~~
## 不要使用new
Dart2使new 关键字可选。即使在Dart 1中,它的含义也一直不清楚,因为工厂构造函数意味着新的调用可能仍然不会返回新的对象。
为了减少迁移的痛苦,这种语言仍然允许使用new,但是要考虑到它已被弃用,并将其从代码中删除。
以下是推荐的写法:
~~~
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(
child: Text('Increment'),
),
Text('Click!'),
],
);
}
~~~
以下是不推荐的写法:
~~~
Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(
child: new Text('Increment'),
),
new Text('Click!'),
],
);
}
~~~
## 不要过多的使用const。
在表达式必须为常量的上下文中,const关键字是隐式的,不需要编写,也不应该编写。这些上下文是内部的任何表达式:
* 一个const字面量集合。
* 一个const构造函数调用
* 一个元数据注释。
* const变量声明的初始化器。
* 在switch-case表达式中——在case的冒号(:)之前,而不是case的主体。
(默认值不包括在这个列表中,因为Dart的未来版本可能会支持非常量默认值。)
Basically, any place where it would be an error to write new instead of const, Dart 2 allows you to omit the const.
基本上,任何使用new而不是const的地方都有可能出现错误,Dart 2允许你省略const。
>**译者注**:以上这句的翻译有点儿拗口。其实通俗的解释是这样的:比如颜色的rgb值[255, 0, 0]这其实是new初始化了一个数组(list集合),但是如果在之前加了const [255, 0, 0]就不对了
以下是正确写法:
~~~
const primaryColors = [
Color("red", [255, 0, 0]),
Color("green", [0, 255, 0]),
Color("blue", [0, 0, 255]),
];
~~~
以下是错误写法:
~~~
const primaryColors = const [
const Color("red", const [255, 0, 0]),
const Color("green", const [0, 255, 0]),
const Color("blue", const [0, 0, 255]),
];
~~~
';
成员
最后更新于:2022-04-02 05:55:05
[TOC]
在Dart中,对象具有成员,成员可以是函数(方法)或数据(实例变量)。以下最佳实践适用于对象的成员。
## 不要把不必要地将字段包装在getter和setter中。
在Java和c#中,通常将所有字段隐藏在getter和setter之后(或c#中的属性),即使实现只是转发给字段。这样,如果您需要在这些成员中做更多的工作,您可以不需要接触callsites。这是因为调用getter方法与访问Java中的字段不同,访问属性与访问c#中的原始字段不兼容。
Dart没有这个限制。字段和getter /setter完全无法区分。您可以在类中公开一个字段,然后将其包装在getter和setter中,而不必接触任何使用该字段的代码。
~~~
class Box {
var contents;
}
~~~
以下是不推荐的写法:
~~~
class Box {
var _contents;
get contents => _contents;
set contents(value) {
_contents = value;
}
}
~~~
## 优先使用final字段来创建只读属性。
如果您有一个字段,外部代码应该能够看到,但不能分配给它,一个简单的解决方案是简单地标记它为final。
~~~
class Box {
final contents = [];
}
~~~
以下是不推荐的写法:
~~~
class Box {
var _contents;
get contents => _contents;
}
~~~
当然,如果您需要在构造函数外部内部分配字段,那么您可能需要执行“私有字段、公共getter”模式,但是在需要之前不要这样做。
## 考虑对简单成员使用=>。
除了对函数表达式使用=>外,Dart还允许使用它定义成员。这种样式非常适合于只计算并返回值的简单成员。
~~~
double get area => (right - left) * (bottom - top);
bool isReady(num time) => minTime == null || minTime <= time;
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
~~~
写代码的人似乎喜欢=>,但很容易滥用它,最终导致难以阅读的代码。如果您的声明超过几行或包含深度嵌套的表达式(级联和条件运算符是常见的攻击者),请您和所有需要阅读您的代码的人帮忙,并使用块体和一些语句。
~~~
Treasure openChest(Chest chest, Point where) {
if (_opened.containsKey(chest)) return null;
var treasure = Treasure(where);
treasure.addAll(chest.contents);
_opened[chest] = treasure;
return treasure;
}
~~~
以下是不推荐的写法:
~~~
Treasure openChest(Chest chest, Point where) =>
_opened.containsKey(chest) ? null : _opened[chest] = Treasure(where)
..addAll(chest.contents);
~~~
对于不返回值的成员,也可以使用=>。如果setter很小,并且具有使用=>的相应getter,那么这就是惯用方法。
~~~
num get x => center.x;
set x(num value) => center = Point(value, center.y);
~~~
对于非setter void成员,使用=>不是一个好主意。=>意味着“返回一个值”,因此如果您使用void成员,读者可能会误解它的作用。
## 在不需要的时候不要用this.以免产生歧义。
JavaScript需要明确这一点。引用当前正在执行其方法的对象上的成员,但是像dart一样的c++、Java和c#没有这个限制。
你唯一需要使用这个的场景是:当具有相同名称的局部变量隐藏您想要访问的成员时。
以下是不推荐的写法:
~~~
class Box {
var value;
void clear() {
this.update(null);
}
void update(value) {
this.value = value;
}
}
~~~
以下是推荐的写法:
~~~
class Box {
var value;
void clear() {
update(null);
}
void update(value) {
this.value = value;
}
}
~~~
注意,构造函数参数从不在构造函数初始化列表中隐藏字段:
~~~
class Box extends BaseBox {
var value;
Box(value)
: value = value,
super(value);
}
~~~
这看起来令人惊讶,但工作起来像你想要的。幸运的是,由于初始化了formals,这样的代码相对少见。
## 在可能的情况下,对字段的声明进行初始化。
如果字段不依赖于任何构造函数参数,则可以并且应该在声明时初始化它。它需要更少的代码,并且确保如果类有多个构造函数,您不会忘记初始化它。
以下是不推荐的写法:
~~~
class Folder {
final String name;
final List contents;
Folder(this.name) : contents = [];
Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
}
~~~
以下是推荐的写法:
~~~
class Folder {
final String name;
final List contents = [];
Folder(this.name);
Folder.temp() : name = 'temporary';
}
~~~
当然,如果字段依赖于构造函数参数,或者由不同的构造函数以不同的方式初始化,那么这个指导原则就不适用。
';
变量
最后更新于:2022-04-02 05:55:02
[TOC]
下面的最佳实践描述了如何在Dart中最好地使用变量。
## 不要显式地将变量初始化为空。
在Dart中,未显式初始化的变量或字段自动被初始化为null。这是由语言可靠地指定的。在Dart中没有“未初始化内存”的概念。添加= null是多余的和不需要的。
~~~
int _nextId;
class LazyId {
int _id;
int get id {
if (_nextId == null) _nextId = 0;
if (_id == null) _id = _nextId++;
return _id;
}
}
~~~
以下是不推荐的写法:
~~~
int _nextId = null;
class LazyId {
int _id = null;
int get id {
if (_nextId == null) _nextId = 0;
if (_id == null) _id = _nextId++;
return _id;
}
}
~~~
## 避免储存你能计算的东西。
在设计类时,您通常希望将多个视图公开到相同的底层状态。通常你会看到在构造函数中计算所有视图的代码,然后存储它们:
应该避免的写法:
~~~
class Circle {
num radius;
num area;
num circumference;
Circle(num radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
~~~
这个代码有两个问题。首先,它可能会浪费内存。严格地说,这个区域和周长是缓存。它们是存储的计算,我们可以从已有的其他数据中重新计算。他们用增加的内存换取减少的CPU使用。我们知道我们有一个性能问题值得权衡吗?
更糟糕的是,代码是错误的。缓存的问题是无效——您如何知道何时缓存过期需要重新计算?在这里,我们永远不会这样做,即使半径是可变的。您可以指定一个不同的值,而面积和周长将保留它们以前的、现在不正确的值。
为了正确处理缓存失效,我们需要这样做:
应该避免的写法:
~~~
class Circle {
num _radius;
num get radius => _radius;
set radius(num value) {
_radius = value;
_recalculate();
}
num _area;
num get area => _area;
num _circumference;
num get circumference => _circumference;
Circle(this._radius) {
_recalculate();
}
void _recalculate() {
_area = pi * _radius * _radius;
_circumference = pi * 2.0 * _radius;
}
}
~~~
这需要编写、维护、调试和读取大量代码。相反,您的第一个实现应该是:
~~~
class Circle {
num radius;
Circle(this.radius);
num get area => pi * radius * radius;
num get circumference => pi * 2.0 * radius;
}
~~~
这段代码更短,占用的内存更少,也更不容易出错。它存储表示圆所需的最小数据量。没有字段可以不同步,因为只有一个真实的源。
在某些情况下,您可能需要缓存慢速计算的结果,但只有在知道存在性能问题之后,才能这样做,请仔细执行,并留下解释优化的注释。
';
参数
最后更新于:2022-04-02 05:55:00
[TOC]
## 使用=将命名参数与其默认值分割开。
由于遗留原因,Dart均允许“:”和“=”作为指定参数的默认值分隔符。为了与可选的位置参数保持一致,使用“=”。
~~~
void insert(Object item, {int at = 0}) { ... }
~~~
以下是不推荐示例:
~~~
void insert(Object item, {int at: 0}) { ... }
~~~
## 不要使用显式默认值null。
如果参数是可选的,但没有给它一个默认值,则语言隐式地使用null作为默认值,因此不需要编写它。
~~~
void error([String message]) {
stderr.write(message ?? '\n');
}
~~~
以下是不推荐的示例:
~~~
void error([String message = null]) {
stderr.write(message ?? '\n');
}
~~~
';
函数的使用
最后更新于:2022-04-02 05:54:58
[TOC]
在Dart中,连函数都是对象。下面是一些涉及函数的最佳实践。
## 使用函数声明将函数绑定到名称。
现代语言已经认识到本地嵌套函数和闭包是多么有用。在另一个函数中定义一个函数是很常见的。在许多情况下,此函数被立即用作回调函数,不需要名称。函数表达式就很好。
但是,如果您确实需要给它一个名称,那么使用函数声明语句而不是将lambda绑定到变量。
~~~
void main() {
localFunction() {
...
}
}
~~~
以下是错误示例:
~~~
void main() {
var localFunction = () {
...
};
}
~~~
## 当可以使用"快速触发"时不要创建lambda函数
>**译者注**:这儿快速触发是"tear-off"的翻译,其本意是撕掉的意思,但是明显在此不能直接翻译。根据这种用法觉得翻译为“快速触发”可能会更好理解点儿。如果不明白可以看下边的描述和例子。
如果在对象上引用了一个方法,但省略了括号,Dart会给你一个“快速触发”——闭包接受与方法相同的参数,并在调用时调用它。
如果您有一个调用方法的函数,其参数与传递给它的参数相同,那么您不需要手动将调用包装在lambda中。
~~~
names.forEach(print);
~~~
以下是错误示例:
~~~
names.forEach((name) {
print(name);
});
~~~
';
集合
最后更新于:2022-04-02 05:54:55
[TOC]
Dart开箱即用地支持四种集合类型:列表、映射、队列和集合。以下最佳实践适用于集合。
## 尽可能使用集合字面量。
有两种方法可以创建一个空的可增长列表:\[\]和list()。同样,有三种方法可以创建空的链接散列映射:{}、map()和LinkedHashMap()。
如果您想要创建一个不可增长的列表,或者其他一些自定义集合类型,那么无论如何,都要使用构造函数。否则,使用漂亮的字面量语法。核心库公开了这些构造函数以方便采用,但是惯用Dart代码没有使用它们。
~~~
var points = [];
var addresses = {};
~~~
不要出现以下写法:
~~~
var points = List();
var addresses = Map();
~~~
如果重要的话,你甚至可以为它们提供一个类型参数。
~~~
var points = [];
var addresses = {};
~~~
不要出现以下写法:
~~~
var points = List();
var addresses = Map();
~~~
注意,这并不适用于这些类的命名构造函数。List.from()、Map.fromIterable()和friends都有它们的用途。同样,如果您正在传递一个size to List()来创建一个不可增长的size,那么使用它是有意义的。
## 不要使用.length查看集合是否为空。
迭代器不要求集合知道它的长度或能提供它。调用.length来查看集合中是否包含任何内容会非常缓慢。
相反,还有一些更快、更易读的getter:. isempty和. isnotempty。使用不需要你否定结果的方法。
~~~
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
~~~
不要出现以下写法:
~~~
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');
~~~
## 考虑使用高阶方法转换序列。
如果您有一个集合,并且希望从中生成一个新的修改后的集合,那么使用.map()、.where()和Iterable上的其他方便的方法通常更短,也更具有声明性。
使用这些而不是for循环,可以清楚地表明您的意图是生成一个新的序列,而不是产生副作用。
~~~
var aquaticNames = animals
.where((animal) => animal.isAquatic)
.map((animal) => animal.name);
~~~
与此同时,这可能会更有效。如果您正在链接或嵌套许多高阶方法,那么编写一段命令式代码可能会更清楚。
## 避免使用带有函数字面量的Iterable.forEach()。
forEach()函数在JavaScript中广泛使用,因为内建的for-in循环没有完成您通常想要的工作。在Dart中,如果你想遍历一个序列,惯用的方法是使用循环。
~~~
for (var person in people) {
...
}
~~~
不要出现以下写法:
~~~
people.forEach((person) {
...
});
~~~
例外情况是,如果您想要做的只是调用某个已经存在的函数,并将每个元素作为参数。在这种情况下,forEach()非常方便。
~~~
people.forEach(print);
~~~
## 不要使用List.from(),除非您打算更改结果的类型。
给定一个迭代,有两种明显的方法可以生成包含相同元素的新列表:
~~~
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
~~~
明显的区别是第一个比较短。重要的区别是第一个保留了原始对象的类型参数:
~~~
// Creates a List:
var iterable = [1, 2, 3];
// Prints "List":
print(iterable.toList().runtimeType);
~~~
不要出现以下写法:
~~~
// Creates a List:
var iterable = [1, 2, 3];
// Prints "List":
print(List.from(iterable).runtimeType);
~~~
如果您想改变类型,那么调用List.from()是很有用的:
~~~
var numbers = [1, 2.3, 4]; // List.
numbers.removeAt(1); // Now it only contains integers.
var ints = List.from(numbers);
~~~
但是,如果您的目标只是复制迭代并保留其原始类型,或者您不关心类型,那么使用toList()。
## 使用whereType()按类型筛选集合。
假设你有一个包含多个对象的列表,你想从列表中得到整数。您可以这样使用where():
不推荐以下写法:
~~~
var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int);
~~~
这是冗长的,但更糟糕的是,它返回一个iterable,其类型可能不是您想要的。在这里的示例中,它返回一个Iterable,尽管您可能想要一个Iterable,因为您要过滤它的类型是它。
有时您会看到通过添加cast()来“纠正(corrects)”上述错误的代码:
不推荐以下写法:
~~~
var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int).cast();
~~~
这是冗长的,会创建两个包装器,带有两个间接层和冗余运行时检查。幸运的是,核心库有这个用例的whereType()方法:
~~~
var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType();
~~~
使用whereType()非常简洁,生成所需类型的迭代,并且没有不必要的包装级别。
## 当相似的操作可以完成时,不要使用cast()。
通常,当您处理一个可迭代或流时,您会对它执行几个转换。最后,您希望生成具有特定类型参数的对象。与其修改对cast()的调用,不如看看现有的转换是否可以改变类型。
如果您已经调用toList(),那么可以用调用List.from()替换它,其中T是您想要的结果列表的类型。
~~~
var stuff = [1, 2];
var ints = List.from(stuff);
~~~
不推荐以下写法:
~~~
var stuff = [1, 2];
var ints = stuff.toList().cast();
~~~
如果您正在调用map(),请给它一个显式类型参数,以便它生成所需类型的迭代。类型推断通常根据传递给map()的函数为您选择正确的类型,但有时需要显式。
~~~
var stuff = [1, 2];
var reciprocals = stuff.map((n) => 1 / n);
~~~
不推荐以下写法:
~~~
var stuff = [1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast();
~~~
## 避免使用cast()。
这是对前一个规则的更温和的概括。有时,您无法使用附近的操作来修复某些对象的类型。即使这样,尽可能避免使用cast()来“更改(change)”集合的类型。
选择以下任何一种:
* 用正确的类型创建它。更改第一次创建集合的地方的代码,使其具有正确的类型。
* 在访问时强制转换元素。如果您立即遍历集合,则在迭代中强制转换每个元素。
* 积极的使用List.from()代替cast()。如果您最终将访问集合中的大多数元素,并且不需要由原始活动对象支持对象,那么可以使用List.from()对其进行转换。
cast()方法返回一个惰性集合,该集合检查每个操作的元素类型。如果您只对几个元素执行一些操作,那么惰性可能是好的。但是在许多情况下,延迟验证和包装的开销超过了好处。
下面是一个使用正确类型创建它的示例:
~~~
List singletonList(int value) {
var list = [];
list.add(value);
return list;
}
~~~
不推荐以下写法:
~~~
List singletonList(int value) {
var list = []; // List.
list.add(value);
return list.cast();
}
~~~
下面是对每个元素的访问:
~~~
void printEvens(List
';
字符串的使用
最后更新于:2022-04-02 05:54:53
[TOC]
以下是在Dart中编写字符串时需要记住的一些最佳实践。
## 使用相邻字符串连接字符串文字。
如果有两个字符串字面值(不是值,而是实际引用的字面值),则不需要使用+连接它们。就像在C和c++中,简单地把它们放在一起就能做到。这是创建一个长字符串很好的方法但是不适用于单独一行。
~~~
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
~~~
以下是错误示例:
~~~
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
'parts are overrun by martians. Unclear which are which.');
~~~
## 优先使用插值来组合字符串和值。
如果您之前是用其他语言做开发的,那么您习惯使用+的长链来构建文字和其他值的字符串。 这在Dart中有效,但使用插值总是更清晰,更简短:
~~~
'Hello, $name! You are ${year - birth} years old.';
~~~
以下是不推荐的写法:
~~~
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
~~~
## 在不需要时,避免在插值中使用花括号。
如果您正在插入一个简单的标识符,而后面没有紧跟更多的字母数字文本,那么{}应该被省略。
~~~
'Hi, $name!'
"Wear your wildest $decade's outfit."
'Wear your wildest ${decade}s outfit.'
~~~
要避免以下的写法:
~~~
'Hi, ${name}!'
"Wear your wildest ${decade}'s outfit."
~~~
';
库的使用
最后更新于:2022-04-02 05:54:51
[TOC]
这些指南帮助您以一致的、可维护的方式将多个文件组成程序。为了使这些指南简洁,他们使用“import”来涵盖import和export指令。这两项准则同样适用。
## 一定要在部分指令中使用字符串。
许多Dart开发人员完全避免使用part。他们发现,当每个库都是一个文件时,他们更容易理解自己的代码。如果您选择使用part将库的一部分分割成另一个文件,Dart要求另一个文件依次指出它属于哪个库。由于遗留原因,Dart允许指令的这一部分使用它所属的库的名称。这使得工具在物理上查找主库文件变得更加困难,并且可能使部分实际上属于哪个库变得模糊不清。
首选的现代语法是使用指向库文件的URI字符串,就像在其他指令中使用的一样。如果你有一些库,my_library.dart,包含:
~~~
library my_library;
part "some/other/file.dart";
~~~
然后part文件应该如下:
~~~
part of "../../my_library.dart";
~~~
而不是:
~~~
part of my_library;
~~~
## 不要导入位于另一个包的src目录中的库。
lib下的src目录被指定为包含包自身实现的私有库。包维护者版本包的方式考虑到了这个约定。他们可以自由地对src下的代码进行全面修改,而不需要对包进行破坏性的修改。
这意味着,如果您导入了其他包的私有库,该包的一个小的、理论上没有中断点的版本可能会破坏您的代码。
## 优先在导入包的lib目录中的库时,选择相对路径。
当从同一包中的另一个库中引用包的lib目录中的库时,可以使用相对URI或显式包:。
例如,假设您的目录结构如下:
~~~
my_package
└─ lib
├─ src
│ └─ utils.dart
└─ api.dart
~~~
如果api.dart想要输入utils.dart,它应该这样做:
~~~
import 'src/utils.dart';
~~~
而不是:
~~~
import 'package:my_package/src/utils.dart';
~~~
没有深刻的理由选择前者——它只是更短,我们希望保持一致。
“在您自己的包的lib目录中”部分很重要。lib中的库可以导入lib(或其子目录中)中的其他库。lib之外的库可以使用相对导入访问lib之外的其他库。
但你不能“cross the streams”。在lib之外的库不应该使用相对导入来访问lib下的库,反之亦然。这样做将破坏Dart正确判断两个库uri是否引用同一库的能力。遵循这两个原则:
* 导入路径不应该包含/lib/。
* 库不应该使用../转义lib目录。
';