Contents

Effective Dart: Design

Contents

这是为库编写一致,可用的API的一些准则.

Names

命名是编写可读性,可维护代码的重要部分. 以下最佳做法可以帮助您实现该目标.

DO use terms consistently.

在整个代码中,对同一事物使用相同的名称. 如果用户可能已经知道您的API之外已有一个先例,请遵循该先例.

pageCount         // A field.
updatePageCount() // Consistent with pageCount.
toSomething()     // Consistent with Iterable's toList().
asSomething()     // Consistent with List's asMap().
Point             // A familiar concept.
renumberPages()      // Confusingly different from pageCount.
convertToSomething() // Inconsistent with toX() precedent.
wrappedAsSomething() // Inconsistent with asX() precedent.
Cartesian            // Unfamiliar to most users.

目的是利用用户已经知道的东西. 这包括他们对问题域本身的了解,核心库的约定以及您自己的API的其他部分. 通过在这些知识的基础上进行构建,可以减少他们必须先获得的新知识,然后才能进行生产.

AVOID abbreviations.

除非缩写比未缩写的术语更普遍,否则不要缩写. 如果您缩写,请正确将其大写 .

pageCount
buildRectangles
IOStream
HttpRequest
numPages    // "num" is an abbreviation of number(of)
buildRects
InputOutputStream
HypertextTransferProtocolRequest

PREFER putting the most descriptive noun last.

最后一个词应该是事物的最描述. 您可以在其前面加上其他词(例如形容词),以进一步描述事物.

pageCount             // A count (of pages).
ConversionSink        // A sink for doing conversions.
ChunkedConversionSink // A ConversionSink that's chunked.
CssFontFaceRule       // A rule for font faces in CSS.
numPages                  // Not a collection of pages.
CanvasRenderingContext2D  // Not a "2D".
RuleFontFaceCss           // Not a CSS.

CONSIDER making the code read like a sentence.

如果对命名有疑问,请编写一些使用您的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);

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

读者的焦点是财产是什么 . 如果用户更关心如何确定属性,则它可能应该是带有动词短语名称的方法.

list.length
context.lineWidth
quest.rampagingSwampBeast
list.deleteItems

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

布尔名称通常在控制流中用作条件,因此您需要一个在此处读得很好的名称. 比较:

if (window.closeable) ...  // Adjective.
if (window.canClose) ...   // Verb.

好名字通常以以下几种动词之一开头:

  • 一表"是": isEnabledwasShownwillFire . 到目前为止,这些是最常见的.

  • an auxiliary verb: hasElements, canClose, shouldConsume, mustSave.

  • 一个活动动词: ignoresInputwroteFile . 这些是罕见的,因为它们通常是模棱两可的. 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,因为这些setter在模板中调用,而不是从其他Dart代码中调用.

CONSIDER omitting the verb for a named boolean parameter.

这完善了先前的规则. 对于布尔型的命名参数,其名称通常不带动词就很清楚,并且代码在调用站点处可读性更好.

Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);

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

大多数布尔名称在概念上都具有"正"和"负"形式,其中前者感觉像是基本概念,而后者则是其否定形式("开放"和"封闭","启用"和"禁用"等).通常,后一种名称从字面上有前缀否定了前者:"看得见"和" -可见光","连接"和" 分散 -连通","零"和" -零".

在选择true代表的两种情况中的哪一种(以及因此为属性命名的情况)时,请选择肯定或更基本的一种. 布尔成员通常嵌套在逻辑表达式内,包括否定运算符. 如果您的媒体资源本身读起来像是一个否定词,那么读者就很难在思想上执行双重否定并理解代码的含义.

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

对于某些属性,没有明显的正形式. 刷新到磁盘上的文档是否"已保存"或" 更改"? 尚未刷新的文档是否 " 保存"或"已更改"? 在模棱两可的情况下,倾向于选择不太可能被用户否定或使用较短名称的选择.

例外:对于某些属性,否定形式是用户非常需要使用的形式. 选择肯定的案例将迫使他们用否定财产! 到处. 取而代之的是,对该属性使用否定大小写可能更好.

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

可调用成员可以将结果返回给调用者并执行其他工作或副作用. 在像Dart这样的命令式语言中,成员通常被称为主要是因为它们的副作用:它们可能会更改对象的内部状态,产生一些输出或与外界对话.

这类成员应使用命令性动词短语来命名,以阐明成员执行的工作.

list.add("element");
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.

其他可调用成员几乎没有副作用,但是会向调用者返回有用的结果. 如果成员不需要参数来执行此操作,则通常应该是一个吸气剂. 但是,有时逻辑"属性"需要一些参数. 例如, elementAt()从集合中返回一条数据,但是它需要一个参数来知道要返回条数据.

这意味着该成员在语法上是一个方法,但从概念上讲它是一个属性,因此应使用描述该成员返回内容的短语来这样命名.

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

该指南故意比前一个指南软. 有时,一种方法没有副作用,但使用诸如list.take()string.split()类的动词短语更容易命名.

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

当成员产生的结果没有任何副作用时,通常应为吸气剂或带有名词短语名称的方法来描述其返回的结果. 但是,有时产生该结果所需的工作很重要. 它可能易于出现运行时故障,或者使用重量级资源,例如网络或文件I / O. 在这种情况下,如果您希望呼叫者考虑该成员正在做的工作,请给该成员一个描述该工作的动词短语名称.

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

但是请注意,该指南比前两个指南要软. 操作执行的工作通常是与调用者无关的实现细节,并且性能和健壮性边界随时间而变化. 在大多数情况下,基于他们为来电者,他们没有怎么做做什么命名的成员.

AVOID starting a method name with get.

在大多数情况下,该方法应该与一个getter get从名称中删除. 例如,而不是使用名为getBreakfastOrder()的方法,而是定义一个名为breakfastOrder的吸气剂.

即使该成员确实需要成为一个方法,因为它需要参数,否则它不适合使用getter,您仍应避免使用get . 像以前的准则所述,可以:

  • 如果调用者主要关心方法返回的值,则只需删除get使用名词短语名称(例如breakfastOrder() .

  • 如果调用者关心正在完成的工作,则使用动词短语名称 ,但选择比get更精确地描述工作的动词,例如createdownloadfetchcalculaterequestaggregate等.

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

Linter规则: use_to_and_as_if_applicable

转换方法是一种返回新对象的方法,该对象包含接收者几乎所有状态的副本,但通常采用某种不同的形式或表示形式. 核心库有一个惯例,这些方法都开始命名to随后样的结果.

如果定义转换方法,则遵循该约定将很有帮助.

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

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

Linter规则: use_to_and_as_if_applicable

转换方法是"快照". 结果对象具有其自己的原始对象状态副本. 还有其他类似转换的方法可以返回视图,它们提供了一个新对象,但该对象引用了原始对象. 以后对原始对象的更改将反映在视图中.

您要遵循的核心库约定是as___() .

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

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

用户将在呼叫站点看到该参数,因此在名称本身中引用该参数通常也不利于可读性.

list.add(element);
map.remove(key);
list.addElement(element)
map.removeKey(key)

但是,提及一个参数以使其与采用不同类型的其他类似名称的方法区别开来可能会很有用:

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

DO follow existing mnemonic conventions when naming type parameters.

单字母名称并不能完全说明问题,但几乎所有通用类型都使用它们. 幸运的是,他们大多以一致的助记符方式使用它们. 约定是:

  • E为集合中的元素类型:

    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • 关联集合中类型的KV

    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • R表示用作函数或类的方法的返回类型的类型. 这并不常见,但有时会出现在typedef中以及实现访问者模式的类中:

    abstract class ExpressionVisitor<R> {
      R visitBinary(BinaryExpression node);
      R visitLiteral(LiteralExpression node);
      R visitUnary(UnaryExpression node);
    }
  • Otherwise, use T, S, and U for generics that have a single type parameter and where the surrounding type makes its meaning obvious. There are multiple letters here to allow nesting without shadowing a surrounding name. For example:

    class Future<T> {
      Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
    }

    在这里,通用方法then<S>()使用S来避免在Future<T>上遮盖T

如果以上情况都不适合,则可以使用另一个单字母助记符名称或描述性名称:

class Graph<N, E> {
  final List<N> nodes = [];
  final List<E> edges = [];
}

class Graph<Node, Edge> {
  final List<Node> nodes = [];
  final List<Edge> edges = [];
}

实际上,现有约定涵盖了大多数类型参数.

Libraries

下划线字符( _ )表示成员是其库的私有成员. 这不只是约定俗成,而是内置于语言本身.

PREFER making declarations private.

图书馆(顶层或类)中的公共声明是其他图书馆可以并且应该访问该成员的信号. 这也是您的图书馆方面的承诺,支持它并在它发生时正常运行.

如果这不是您想要的,请添加小_并感到高兴. 狭窄的公共界面更易于维护和用户学习. 作为一个不错的好处,分析器会告诉您有关未使用的私有声明的信息,以便您可以删除无效代码. 如果成员是公共成员,则无法执行此操作,因为它不知道其视图外是否有任何代码在使用它.

CONSIDER declaring multiple classes in the same library.

某些语言(例如Java)将文件的组织与类的组织联系在一起-每个文件只能定义一个顶级类. Dart没有此限制. 库是与类分离的不同实体. 如果单个库包含多个类,顶级变量和函数,如果它们在逻辑上都属于同一类,则完全可以.

将多个类一起放在一个库中可以启用一些有用的模式. 由于Dart的隐私权是在库级别而非类级别起作用的,因此这是一种定义"朋友"类的方法,就像在C ++中一样. 在同一个库中声明的每个类都可以访问彼此的私有成员,但是该库外部的代码无法访问.

当然,该指南并不意味着您应该将所有类都放入一个庞大的整体库中,而只是允许您在一个库中放置多个类.

Classes and mixins

Dart是一种"纯"的面向对象语言,因为所有对象都是类的实例. 但是Dart不需要在类内定义所有代码-您可以像在过程语言或函数语言中一样定义顶级变量,常量和函数.

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

Linter规则: one_member_abstracts

与Java不同,Dart具有一流的函数,闭包以及使用它们的优美语法. 如果您只需要像回调之类的东西,则只需使用一个函数即可. 如果您要定义一个类,并且它只有一个抽象成员,且其名称无意义,例如callinvoke ,则很有可能只需要一个函数.

typedef Predicate<E> = bool Function(E element);
abstract class Predicate<E> {
  bool test(E element);
}

AVOID defining a class that contains only static members.

Linter规则: void_classes_with_only_static_members

在Java和C#中,每个定义都必须在一个类内,因此通常会看到"类"仅作为填充静态成员的位置而存在. 其他类用作名称空间-一种为一堆成员赋予共享前缀以使其相互关联或避免名称冲突的方法.

达特拥有顶级函数,变量和常量,所以你并不需要一个类只是为了定义的东西. 如果您想要的是名称空间,则最好使用库. 库支持导入前缀和显示/隐藏组合器. 这些功能强大的工具可让您的代码使用者以最适合他们的方式处理名称冲突.

如果函数或变量在逻辑上不与类绑定,则将其放在顶层. 如果您担心名称冲突,请给它一个更精确的名称,或将其移到可以使用前缀导入的单独库中.

DateTime mostRecent(List<DateTime> dates) {
  return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}

const _favoriteMammal = 'weasel';
class DateUtils {
  static DateTime mostRecent(List<DateTime> 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';
}

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

如果将构造函数从生成构造函数更改为工厂构造函数,则调用该构造函数的任何子类构造函数都会中断. 同样,如果一个类更改了它在this调用的自身方法,则可能会破坏覆盖这些方法的子类,并期望它们在某些时候被调用.

这两者都意味着一个类需要仔细考虑是否要允许子类化. 这可以在文档注释中传达,也可以通过给类一个明显的名称(如IterableBase来传达. 如果类的作者不这样做,它的最好假设你应该扩展的类. 否则,以后对其进行更改可能会破坏您的代码.

DO document if your class supports being extended.

这是上述规则的必然结果. 如果要允许类的子类,请说明. 用Base后缀的类名,或者在类的文档注释中提及它.

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

隐式接口是Dart中一个功能强大的工具,可以避免在可以从该合同的实现签名中轻易推断出该类的合同时重复该合同.

但是,实现类的接口与该类的联系非常紧密. 这实际上意味着对要实现其接口的类的任何更改都会破坏您的实现. 例如,将新成员添加到班级通常是安全的,不变的更改. 但是,如果您正在实现该类的接口,则您的类现在会出现静态错误,因为它缺少该新方法的实现.

图书馆维护者需要在不破坏用户的前提下发展现有类的能力. 如果您将每个类都视为公开用户可以自由实现的接口,那么更改这些类将变得非常困难. 这种困难又意味着您依赖的库增长较慢,无法适应新需求.

为了给类的作者更多的使用余地,请避免实现隐式接口,但明确打算实现的类除外. 否则,您可能会引入作者不希望的耦合,并且它们可能会破坏您的代码而没有意识到.

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

如果您的课程可用作接口,请在课程的文档注释中提及.

DO use mixin to define a mixin type.

短绒规则: prefer_mixin

Dart最初没有单独的语法来声明要混入其他类的类. 相反,任何满足某些限制的类(没有非默认构造函数,没有超类等)都可以用作混合. 这令人困惑,因为该类的作者可能不希望将其混入.

Dart 2.1.0添加了mixin关键字,用于显式声明mixin. 使用创建的类型只能用作mixin,并且该语言还可以确保您的mixin不受限制. 定义打算用作混合类型的新类型时,请使用以下语法.

mixin ClickableMixin implements Control {
  bool _isDown = false;

  void click();

  void mouseDown() {
    _isDown = true;
  }

  void mouseUp() {
    if (_isDown) click();
    _isDown = false;
  }
}

您可能仍然会遇到使用class定义mixin的旧代码,但是首选新语法.

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

短绒规则: prefer_mixin

为了兼容性,Dart仍然允许您混入未使用mixin定义的类. 但是,这很冒险. 如果该类的作者不希望将该类用作混入,则他们可能会以打破混入限制的方式更改该类. 例如,如果他们添加一个构造函数,则您的类将中断.

如果类没有文档注释或类似IterableMixin的明显名称,假设如果没有使用mixin声明的话,您不能混入类.

Constructors

Dart构造函数是通过声明一个与该类同名的函数以及一个可选的附加标识符来创建的. 后者称为命名构造函数 .

CONSIDER making your constructor const if the class supports it.

如果您有一个所有字段都为final的类,并且构造函数只对它们进行了初始化,则可以使该构造const成为const . 这使用户可以在需要常量的地方(其他较大的常量,切换用例,默认参数值等)中创建类的实例.

如果您未明确将其设为const ,那么他们将无法做到这一点.

但是请注意, const构造函数是您的公共API中的承诺. 如果以后将构造const更改为non- const ,它将破坏用常量表达式调用它的用户. 如果您不想这样做,请不要将其设为const . 实际上, const构造函数对于简单,不可变的数据记录类最有用.

Members

成员属于对象,可以是方法或实例变量.

PREFER making fields and top-level variables final.

短绒规则: preferred_final_fields

对于程序员来说,不可变的状态(不会随时间变化)是更容易理解的状态. 最小化使用它们的可变状态数量的类和库往往更易于维护.

Of course, it is often useful to have mutable data. But, if you don’t need it, your default should be to make fields and top-level variables final when you can.

DO use getters for operations that conceptually access properties.

确定成员何时应该是吸气剂而不是方法是一个好的API设计的挑战,微妙但重要的部分,因此,这一指南很长. 其他一些语言的文化也远离吸气剂. 它们仅在操作几乎完全像字段时使用它们-它对完全存在于对象上的状态进行微不足道的计算. 比该名称更复杂或重量级的任何内容都将在名称后加上()来表示"此处将要进行计算!",因为在.之后有一个裸名. 表示"字段".

飞镖不是那样的. 在Dart中, 所有点分名称都是可以进行计算的成员调用. 字段很特殊-它们是由语言提供实现的getter. 换句话说,吸气剂在Dart中不是"特别慢的领域"; 领域是"特别快速的吸气剂".

即使这样,在方法上选择吸气剂也会向调用方发送重要信号. 大致而言,信号表明操作是"类似场的". 据调用者所知,至少原则上可以使用字段来实现该操作. 这意味着:

  • 该操作不接受任何参数,并返回结果.

  • 呼叫者主要关心结果. If you want the caller to worry about how the operation produces its result more than they do the result being produced, then give the operation a verb name that describes the work and make it a method.

    并不意味着该操作必须是特别快,为了成为一个getter. IterableBase.lengthO(n) ,这没关系. 吸气剂进行大量计算很好. 但是,如果这样做的工作量惊人 ,您可能希望通过使其成为一种名称来描述其功能的动词的方法来引起他们的注意.

    connection.nextIncomingMessage; // Does network I/O.
    expression.normalForm; // Could be exponential to calculate.
  • 该操作没有用户可见的副作用. 访问实数字段不会更改对象或程序中的任何其他状态. 它不会产生输出,写入文件等.吸气剂也不应执行这些操作.

    "用户可见"部分很重要. 吸气剂可以修改隐藏状态或产生带外副作用,这是很好的. Getter可以懒惰地计算和存储其结果,写入缓存,记录日志等.只要调用方不关心副作用,就可以了.

    stdout.newline; // Produces output.
    list.clear; // Modifies object.
  • 该操作是幂等的 . "幂等"是一个奇数词,在此情况下,基本上意味着多次调用该操作每次都会产生相同的结果,除非在这些调用之间明确修改了某些状态. (显然,如果在list.length调用之间向列表中添加元素, list.length会产生不同的结果.)

    这里的"相同结果"并不意味着获取器必须在连续调用中从字面上产生相同的对象. 要求这样做将迫使许多吸气剂进行脆性缓存,从而抵消了使用吸气剂的全部要点. 每次调用吸气剂返回新的未来或列出它,这是很正常的事情,而且非常好. 重要的是,将来的完成价值相同,并且列表包含相同的元素.

    换句话说,结果值在调用者关心的方面应该相同.

    DateTime.now; // New result each time.
  • 生成的对象不会公开所有原始对象的状态. 字段仅显示一个对象. 如果您的操作返回的结果暴露了原始对象的整个状态,则最好使用to___()as___()方法.

如果以上所有都描述了您的操作,那应该是一种吸气剂. 似乎很少有成员能幸免于难,但令人惊讶的是很多人都能幸免. 许多操作只是对某些状态进行一些计算,而大多数操作可以并且应该是吸气剂.

rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;

DO use setters for operations that conceptually change properties.

短线规则: use_setters_to_change_properties

在setter和方法之间进行决定类似于在getter与方法之间进行决定. 在这两种情况下,操作都应"类似现场".

对于二传手,"像田野"的意思是:

  • 该操作采用单个参数,并且不产生结果值.

  • 该操作会更改对象中的某些状态.

  • 该操作是幂等的. 就调用者而言,用相同的值两次调用相同的setter不应再次执行任何操作. 在内部,也许您有一些缓存失效或正在进行日志记录. 没关系. 但是从呼叫者的角度来看,第二个呼叫似乎没有任何作用.

rectangle.width = 3;
button.visible = false;

DON’T define a setter without a corresponding getter.

短绒规则: void_setters_without_getters

Users think of getters and setters as visible properties of an object. A “dropbox” property that can be written to but not seen is confusing and confounds their intuition about how properties work. For example, a setter without a getter means you can use = to modify it, but not +=.

该指导方针并不意味着你应该添加一个getter只是允许您要添加的制定者. 对象通常不应暴露超出其所需状态的状态. 如果您可以修改某个对象的某些状态,但不能以相同的方式公开它,请改用一种方法.

Exception: An Angular component class may expose setters that are invoked from a template to initialize the component. Often, these setters are not intended to be invoked from Dart code and don’t need a corresponding getter. (If they are used from Dart code, they should have a getter.)

AVOID returning null from members whose return type is bool, double, int, or num.

短绒规则: void_returning_null

即使在Dart中所有类型都可以为空,用户仍认为这些类型几乎不包含null ,并且小写字母名称鼓励使用" Java原语"思维方式.

在您的API中使用"可空基元"类型有时会很有用,例如,表示映射中某些键不存在值,但这种情况很少见.

如果您确实有一个可能返回null的成员,请非常清楚地记录下来,包括将返回null的条件.

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

短绒规则: void_returning_this

方法级联是链接方法调用的更好解决方案.

var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');
var buffer = StringBuffer()
    .write('one')
    .write('two')
    .write('three');

Types

当您在程序中写下类型时,会限制流入代码不同部分的值的类型. 类型可以出现在两种地方:声明的类型注释泛型调用的类型参数.

当您想到"静态类型"时,通常会想到类型注释. 您可以键入注释变量,参数,字段或返回类型. 在下面的示例中, boolString是类型注释. 它们与代码的静态声明性结构无关,并且不会在运行时"执行".

bool isEmpty(String parameter) {
  bool result = parameter.isEmpty;
  return result;
}

通用调用是集合文字,对通用类的构造函数的调用或通用方法的调用. 在下一个示例中, numint是通用调用的类型参数. 即使它们是类型,它们还是一流的实体,它们在运行时会得到验证并传递给调用.

var lists = <num>[1, 2];
lists.addAll(List<num>.filled(3, 4));
lists.cast<int>();

我们在这里强调"通用调用"部分,因为类型参数也可以出现在类型注释中:

List<int> ints = [1, 2];

在这里, int是类型参数,但它出现在类型注释内,而不是通用调用内. 您通常不必担心这种区别,但是在几个地方,对于在通用调用中使用类型而不是在类型注释中使用类型,我们有不同的指导.

在大多数地方,Dart允许您省略类型注释,并根据附近的上下文为您推断类型,或者默认为dynamic类型. Dart同时具有类型推断和dynamic类型的事实导致人们对代码"未类型化"的含义感到困惑. 这是否意味着代码是动态键入的,或者您没有编写类型? 为避免这种混淆,我们避免说" untyped",而使用以下术语:

  • 如果代码带有类型注释 ,则该类型已明确地写入代码中.

  • 如果代码被推断出 ,则不会编写任何类型注释,并且Dart会成功地自行找出类型. 推理可能会失败,在这种情况下,准则不会将其视为推理. 在某些地方,推理失败是一个静态错误. 在其他情况下,Dart使用dynamic作为后备类型.

  • 如果代码是dynamic ,则其静态类型是特殊的dynamic类型. 可以将代码显式地注释为dynamic ,也可以对其进行推断.

换句话说,是注释代码还是推断代码与dynamic还是其他类型正交.

推理是一个强大的工具,可让您省去编写和读取明显或不感兴趣的类型的工作. 在明显的情况下,如果类型很重要(例如强制类型转换),则省略类型也会引起读者的注意.

显式类型也是健壮,可维护代码的关键部分. 它们定义了API的静态形状. 他们记录并强制执行允许哪些值进入程序的不同部分.

这里的指南在简洁与明晰,灵活性与安全性之间找到了最佳平衡. 在决定写哪种类型时,您需要回答两个问题:

  • 我应该写哪种类型,因为我认为最好在代码中显示它们?
  • 由于推论无法为我提供这些类型,我该怎么写?

这些准则可帮助您回答第一个问题:

这些内容涵盖了第二个:

其余准则涵盖有关类型的其他更具体的问题.

PREFER type annotating public fields and top-level variables if the type isn’t obvious.

Linter规则: type_annotate_public_apis

类型注释是有关如何使用库的重要文档. 它们在程序区域之间形成边界,以隔离类型错误的来源. 考虑:

install(id, destination) => ...

在这里,不清楚是什么id . 弦吗? destination是什么? 字符串还是File对象? 此方法是同步的还是异步的? 这更清楚:

Future<bool> install(PackageId id, String destination) => ...

但是,在某些情况下,类型是如此明显,以至于编写它是毫无意义的:

const screenWidth = 640; // Inferred as int.

"明显"的定义不明确,但都是不错的选择:

  • Literals.
  • 构造函数调用.
  • 引用其他显式键入的常量.
  • 关于数字和字符串的简单表达式.
  • 读者应该熟悉的工厂方法,例如int.parse()Future.wait()等.

如有疑问,请添加类型注释. 即使类型很明显,您仍可能希望显式注释. 如果推断出的类型依赖于价值或声明的其他库,您可能需要键入注释你的宣言,这样的改变,其他图书馆没有后台更改自己的API的类型没有你实现.

CONSIDER type annotating private fields and top-level variables if the type isn’t obvious.

Linter规则: preferred_typing_uninitialized_variables

公共声明上的类型注释可帮助您的代码用户 . 私人成员的类型可以帮助维护者 . 私有声明的范围较小,那些需要了解声明类型的人也更可能熟悉周围的代码. 这使得合理地更多地依赖于私有声明的推断和省略类型,这就是为什么该指南比前一个指南更软的原因.

如果您认为初始化程序表达式(无论它是什么)足够清楚,则可以省略注释. 但是,如果您认为注释有助于使代码更清晰,请添加注释.

AVOID type annotating initialized local variables.

Linter规则: omit_local_variable_types

局部变量,特别是在现代的代码中,函数往往很小,它们的作用域很小. 省略类型会将读者的注意力集中在变量的更重要的名称及其初始化值上.

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

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

  return desserts;
}

如果局部变量没有初始化程序,则无法推断其类型. 在这种情况下,注释一个好主意. 否则,您将变得dynamic并失去静态类型检查的好处.

List<AstNode> parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}

AVOID annotating inferred parameter types on function expressions.

Linter规则: void_types_on_closure_parameters

匿名函数几乎总是立即传递给采用某种类型的回调的方法. (如果不立即使用该函数,通常值得将其命名为声明.)在类型化上下文中创建函数表达式时,Dart会尝试根据期望的类型来推断函数的参数类型.

例如,当您将函数表达式传递给Iterable.map() ,将根据map()期望的回调类型来推断函数的参数类型:

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

在极少数情况下,周围的环境不够精确,无法为一个或多个函数的参数提供类型. 在这种情况下,您可能需要注释.

AVOID redundant type arguments on generic invocations.

如果推断将填充相同的类型,则类型参数是多余的. 如果调用是带类型注释的变量的初始化程序,或者是函数的参数,则通常为您填充类型:

Set<String> things = Set();
Set<String> things = Set<String>();

在这里,变量的类型注释用于推断初始化程序中构造函数调用的类型参数.

在其他情况下,没有足够的信息来推断类型,因此您应该编写type参数:

var things = Set<String>();
var things = Set();

在这里,由于变量没有类型注释,所以没有足够的上下文来确定要创建哪种Set ,因此应显式提供type参数.

DO annotate when Dart infers the wrong type.

有时,Dart会推断出类型,而不是您想要的类型. 例如,您可能希望变量的类型是初始化程序的类型的超类型,以便以后可以为该变量分配其他同级类型:

num highScore(List<num> scores) {
  num highest = 0;
  for (var score in scores) {
    if (score > highest) highest = score;
  }
  return highest;
}
num highScore(List<num> scores) {
  var highest = 0;
  for (var score in scores) {
    if (score > highest) highest = score;
  }
  return highest;
}

在这里,如果scores包含双打,如[1.2]然后分配到highest会因为它的推断类型是失败int ,没有num . 在这些情况下,显式注释是有意义的.

PREFER annotating with dynamic instead of letting inference fail.

Dart允许您在许多地方省略类型注释,并尝试为您推断类型. 在某些情况下,如果推论失败,它会默默地为您提供dynamic . 如果您要的是dynamic类型,那么从技术上讲,这是最简洁的获取方式.

但是,这不是最清晰的方法. 偶然的代码阅读者看到缺少注释,无法知道您是希望它是dynamic ,还是要使用其他类型的预期推断,或者只是忘记编写注释.

dynamic类型是您想要的类型时,显式编写它可以使您的意图明确.

dynamic mergeJson(dynamic original, dynamic changes) => ...
mergeJson(original, changes) => ...

PREFER signatures in function type annotations.

没有任何返回类型或参数签名的标识符Function本身是指特殊的Function类型. 这种类型仅比使用dynamic稍微有用. 如果要进行注释,则最好使用包含函数的参数和返回类型的完整函数类型.

bool isValid(String value, bool Function(String) test) => ...
bool isValid(String value, Function test) => ...

例外:有时,您需要一个表示多个不同函数类型的并集的类型. 例如,您可以接受带有一个参数的函数或带有两个参数的函数. 由于我们没有联合类型,因此无法精确地键入该类型,通常必须使用dynamic . Function至少比这有用:

void handleError(void Function() operation, Function errorHandler) {
  try {
    operation();
  } catch (err, stack) {
    if (errorHandler is Function(Object)) {
      errorHandler(err);
    } else if (errorHandler is Function(Object, StackTrace)) {
      errorHandler(err, stack);
    } else {
      throw ArgumentError("errorHandler has wrong signature.");
    }
  }
}

DON’T specify a return type for a setter.

短绒规则: void_return_types_on_setters

设置员总是在Dart中返回void . 写这个词是没有意义的.

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

DON’T use the legacy typedef syntax.

Linter规则: preferred_generic_function_type_aliases

Dart有两种表示法,用于为函数类型定义命名的typedef. 原始语法如下:

typedef int Comparison<T>(T a, T b);

该语法有两个问题:

  • 无法为通用函数类型分配名称. 在上面的示例中,typedef本身是通用的. 如果在代码中引用Comparison ,没有类型参数,则隐式获得函数类型int Function(dynamic, dynamic)而不是 int Function<T>(T, T) . 这在实践中通常不会出现,但在某些特殊情况下很重要.

  • 参数中的单个标识符被解释为参数的名称 ,而不是其类型 . 鉴于:

    typedef bool TestNumber(num);

    大多数用户期望这是一个接受num并返回bool的函数类型. 它实际上是一种函数类型,它可以接收任何对象( dynamic )并返回bool . 参数的名称 (除typedef中的文档外,不用于其他任何参数)为" num". 这是Dart中长期存在的错误源.

The new syntax looks like this:

typedef Comparison<T> = int Function(T, T);

如果要包括参数名称,也可以这样做:

typedef Comparison<T> = int Function(T a, T b);

新语法可以表达旧语法可以表达的更多内容,并且不存在容易出错的功能,在该功能中,单个标识符被视为参数的名称而不是其类型. 在类型注释出现的任何地方,在typedef中的=之后也可以使用相同的函数类型语法,这为我们提供了一种在程序中任何地方编写函数类型的一致方法.

仍支持旧的typedef语法,以避免破坏现有代码,但已弃用.

PREFER inline function types over typedefs.

Linter rule: avoid_private_typedef_functions

在Dart 1中,如果要对字段,变量或泛型类型参数使用函数类型,则必须首先为其定义typedef. Dart 2支持函数类型语法,可在允许使用类型注释的任何地方使用:

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仍然值得. 但是在大多数情况下,用户希望查看实际使用的函数类型正确,并且函数类型语法使他们更加清楚.

CONSIDER using function type syntax for parameters.

Linter规则: use_function_type_syntax_for_parameters

Dart在定义类型为函数的参数时具有特殊的语法. 就像在C中一样,您将参数的名称包含在函数的返回类型和参数签名中:

Iterable<T> where(bool predicate(T element)) => ...

在Dart 2添加函数类型语法之前,这是在不定义typedef的情况下为参数赋予函数类型的唯一方法. 现在,Dart对函数类型有了通用的表示法,您也可以将其用于函数类型的参数:

Iterable<T> where(bool Function(T) predicate) => ...

新语法稍微有些冗长,但与必须使用新语法的其他位置一致.

DO annotate with Object instead of dynamic to indicate any object is allowed.

某些操作适用于任何可能的对象. 例如, log()方法可以采用任何对象并在其上调用toString() . Dart中的两种类型允许所有值: Objectdynamic . 但是,它们传达的是不同的东西. 如果只想声明允许所有对象,请像在Java或C#中那样使用Object .

使用dynamic发送更复杂的信号. 这可能意味着Dart的类型系统不够复杂,无法表示允许的类型集,或者值来自互操作或静态类型系统的权限之外,或者您明确希望在运行时具有动态性.程序中的那一点.

void log(Object object) {
  print(object.toString());
}

/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(dynamic arg) {
  if (arg is bool) return arg;
  if (arg is String) return arg == '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.

当您有一个不返回值的同步函数时,可以使用void作为返回类型. 不产生值但调用者可能需要等待的方法的异步等效项是Future<void> .

您可能会看到使用FutureFuture<Null>代码,因为较早版本的Dart不允许将void作为类型参数. 现在,您应该使用它. 这样做更直接地与您键入类似的同步函数的方式匹配,并且可以为调用方和函数主体提供更好的错误检查.

对于不返回有用值且不需要调用者等待异步工作或处理异步故障的异步函数,请使用void的返回类型.

AVOID using FutureOr<T> as a return type.

如果方法接受FutureOr<int> ,则它接受的内容很慷慨 . 用户可以使用intFuture<int>调用该方法,因此他们无需在Future中包装您将要解包的int .

如果返回 FutureOr<int> ,则用户在执行任何有用的操作之前,需要检查是否返回intFuture<int> . (或者他们只是await值,实际上总是将其视为Future .)只需返回Future<int> ,它就更干净了. 用户更容易理解,一个函数要么总是异步的,要么总是同步的,但是很难正确使用一个函数.

Future<int> triple(FutureOr<int> value) async => (await value) * 3;
FutureOr<int> triple(FutureOr<int> value) {
  if (value is int) return value * 3;
  return (value as Future<int>).then((v) => v * 3);
}

这个指南的更精确的制剂是只使用FutureOr<T>逆变位置. 参数是协变的,返回类型是协变的. 在嵌套函数类型中,这会被翻转—如果您有一个其类型本身就是函数的参数,则回调函数的返回类型现在处于相反位置,而回调函数的参数则为协变量. 这意味着回调的类型可以返回FutureOr<T>

Stream<S> asyncMap<T, S>(
    Iterable<T> iterable, FutureOr<S> Function(T) callback) async* {
  for (var element in iterable) {
    yield await callback(element);
  }
}

Parameters

在Dart中,可选参数可以是位置参数或名称,但不能同时使用.

AVOID positional boolean parameters.

Linter rule: avoid_positional_boolean_parameters

与其他类型不同,布尔值通常以文字形式使用. 像数字之类的东西通常都包装在命名常量中,但是我们通常只是直接传递truefalse . 如果不清楚布尔值表示什么,则可能会使callites不可读:

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;

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

可选的位置参数应具有逻辑顺序,以使较早的参数比较晚的参数更频繁地传递. 用户几乎永远不需要显式地传递一个"空洞"来省略较早的位置参数而传递较晚的位置参数. 最好使用命名参数.

String.fromCharCodes(Iterable<int> 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});

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

如果用户在逻辑上省略了一个参数,则可以通过使该参数成为可选参数,而不是强迫他们传递null ,空字符串或其他表示"未传递"的特殊值,而让他们实际上忽略它.

省略该参数会更简洁,并有助于防止在用户认为自己提供真实值时意外传递诸如null的前哨值的错误.

var rest = string.substring(start);
var rest = string.substring(start, null);

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'

在此处保持一致特别重要,因为这些参数通常是未命名的. 如果您的API使用的是长度而不是终点,那么差异根本不会在调用站点上看到.

Equality

为类实现自定义相等行为可能很棘手. 用户对对象需要匹配的相等性如何工作具有深刻的直觉,而哈希表等集合类型具有希望元素遵循的细微约定.

DO override hashCode if you override ==.

Linter规则: hash_and_equals

默认的哈希码实现提供了一个身份哈希-两个对象通常只有完全相同的对象才具有相同的哈希码. 同样, ==的默认行为是身份.

如果您覆盖== ,则意味着您可能有不同的对象,这些对象被类视为"相等". 相等的任何两个对象必须具有相同的哈希码. 否则,地图和其他基于哈希的集合将无法识别这两个对象是等效的.

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

等价关系应为:

  • 反身a == a应该总是返回true .

  • 对称a == b应该返回与b == a相同b == a .

  • Transitive: If a == b and b == c both return true, then a == c should too.

使用==用户和代码希望遵守所有这些法律. 如果您的班级不能遵守这些规则,则==不是您要表达的操作的正确名称.

AVOID defining custom equality for mutable classes.

Linter规则: void_equals_and_hash_code_on_mutable_classes

当定义== ,还必须定义hashCode . 两者都应考虑对象的字段. 如果这些字段更改,则表明对象的哈希码可以更改.

大多数基于散列的集合都不希望这样-他们假设对象的散列代码永远是相同的,并且如果这不是真的,可能会发生不可预测的行为.

DON’T check for null in custom == operators.

Linter规则: void_null_checks_in_equality_operators

语言指定自动执行此检查,并且仅当右侧不为null时才调用==方法.

class Person {
  final String name;
  // ···
  bool operator ==(other) => other is Person && name == other.name;

  int get hashCode => name.hashCode;
}
class Person {
  final String name;
  // ···
  bool operator ==(other) => other != null && ...
}

by  ICOPY.SITE