『Flutter开发实战』一小时掌握Dart语言

 

参考:https://dart.cn/guides/language/language-tour

Dart 开发语言概览

本文将从变量和运算符开始到类和库的使用来向你介绍 Dart 编程语言的主要功能,这里假设你已经有使用其它语言进行编程的经验。

你可以通过查看 Dart 库概览 学习更多关于 Dart 核心库的知识。若还想了解更多有关语言功能的详细内容,请参阅 Dart 编程语言规范

 备忘:

你可以通过 DartPad 体验 Dart 的大部分语言功能 (了解更多), 打开 DartPad。

本页面内嵌了一些 DartPads 做例子展示,

如果你只看到了空白的框框(而没有任何内容),请查阅 DartPad 常见问题页面

一个简单的 Dart 程序

下面的应用程序代码用到了很多 Dart 的基本功能:

// Define a function.
void printInteger(int aNumber) {
  print('The number is $aNumber.'); // Print to console.
}

// This is where the app starts executing.
void main() {
  var number = 42; // Declare and initialize a variable.
  printInteger(number); // Call a function.
}

下面是上述应用程序中使用到的代码片段,这些代码片段适用于所有(或几乎所有)的 Dart 应用:

// This is a comment.

// 注释。

以双斜杠开头的一行语句称为单行注释。Dart 同样支持多行注释和文档注释。查阅注释获取更多相关信息。

void

一种特殊的类型,表示一个值永远不会被使用。类似于 main() 和 printInteger() 的函数,以 void 声明的函数返回类型,并不会返回值。

int

另一种数据类型,表示一个整型数字。 Dart 中一些其他的内置类型包括 StringList 和 bool

42

表示一个数字字面量。数字字面量是一种编译时常量。

print()

一种便利的将信息输出显示的方式。

'...' (或 "...")

表示字符串字面量。

$variableName (或 ${expression})

表示字符串插值:字符串字面量中包含的变量或表达式。查阅字符串获取更多相关信息。

main()

一个特殊且 必须的 顶级函数,Dart 应用程序总是会从该函数开始执行。查阅 main() 函数 获取更多相关信息。

var

用于定义变量,通过这种方式定义变量不需要指定变量类型。

 备忘:

本站的代码遵循 Dart 风格指南 中的约定。

重要概念

当你在学习 Dart 语言时, 应该牢记以下几点:

  • 所有变量引用的都是 对象,每个对象都是一个  的实例。数字、函数以及 null 都是对象。所有的类都继承于 Object 类。

  • 尽管 Dart 是强类型语言,但是在声明变量时指定类型是可选的,因为 Dart 可以进行类型推断。在上述代码中,变量 number 的类型被推断为 int 类型。如果想显式地声明一个不确定的类型,可以使用特殊类型 dynamic

  • Dart 支持泛型,比如 List<int>(表示一组由 int 对象组成的列表)或 List<dynamic>(表示一组由任何类型对象组成的列表)。

  • Dart 支持顶级函数(例如 main 方法),同时还支持定义属于类或对象的函数(即 静态 和 实例方法)。你还可以在函数中定义函数(嵌套 或 局部函数)。

  • Dart 支持顶级 变量,以及定义属于类或对象的变量(静态和实例变量)。实例变量有时称之为域或属性。

  • Dart 没有类似于 Java 那样的 publicprotected 和 private 成员访问限定符。如果一个标识符以下划线 (_) 开头则表示该标识符在库内是私有的。可以查阅 库和可见性 获取更多相关信息。

  • 标识符 可以以字母或者下划线 (_) 开头,其后可跟字符和数字的组合。

  • Dart 中 表达式 和 语句 是有区别的,表达式有值而语句没有。比如条件表达式 expression condition ? expr1 : expr2 中含有值 expr1 或 expr2。与 if-else 分支语句相比,if-else 分支语句则没有值。一个语句通常包含一个或多个表达式,但是一个表达式不能只包含一个语句。

  • Dart 工具可以显示 警告 和 错误 两种类型的问题。警告表明代码可能有问题但不会阻止其运行。错误分为编译时错误和运行时错误;编译时错误代码无法运行;运行时错误会在代码运行时导致异常

 备忘:

如果您好奇 Dart 为什么使用下划线而不使用诸如 public 或 private 作为修饰符,请参阅 SDK 议题 #33383

关键字

下面的表格中列出了 Dart 语言所使用的关键字。

abstract 2elseimport 2super
as 2enuminswitch
assertexport 2interface 2sync 1
async 1extendsisthis
await 3extension 2library 2throw
breakexternal 2mixin 2true
casefactory 2newtry
catchfalsenulltypedef 2
classfinalon 1var
constfinallyoperator 2void
continueforpart 2while
covariant 2Function 2rethrowwith
defaultget 2returnyield 3
deferred 2hide 1set 2 
doifshow 1 
dynamic 2implements 2static 2 

应该避免使用这些单词作为标识符。但是,带有上标的单词可以在必要的情况下作为标识符:

  • 带有上标 1 的关键字为 上下文关键字,只有在特定的场景才有意义,它们可以在任何地方作为有效的标识符。

  • 带有上标 2 的关键字为 内置标识符,其作用只是在JavaScript代码转为Dart代码时更简单,这些关键字在大多数时候都可以作为有效的标识符,但是它们不能用作类名或者类型名或者作为导入前缀使用。

  • 带有上标 3 的关键字为 Dart 1.0 发布后用于 支持异步 相关内容。不能在由关键字 asyncasync* 或 sync* 标识的方法体中使用 await 或 yield 作为标识符。

其它没有上标的关键字为 保留字,均不能用作标识符。

变量

下面的示例代码将创建一个变量并将其初始化:

var name = 'Bob';

变量仅存储对象的引用。这里名为 name 的变量存储了一个 String 类型对象的引用,“Bob” 则是该对象的值。

name 变量的类型被推断为 String,但是你可以为其指定类型。如果一个对象的引用不局限于单一的类型,可以根据设计指南将其指定为 Object 或 dynamic 类型。

dynamic name = 'Bob';

除此之外你也可以指定类型:

String name = 'Bob';

 备忘:

本文遵循 风格建议指南 中的建议,通过 var 声明局部变量而非使用指定的类型。

默认值

在 Dart 中,未初始化的变量拥有一个默认的初始化值:null。即便数字也是如此,因为在 Dart 中一切皆为对象,数字也不例外。

int lineCount;
assert(lineCount == null);

 备忘:

assert() 的调用将会在生产环境的代码中被忽略掉。在开发过程中,assert(condition) 将会在 条件判断 为 false 时抛出一个异常。详情请查阅 Assert

Final 和 Const

如果你不想更改一个变量,可以使用关键字 final 或者 const 修饰变量,这两个关键字可以替代 var 关键字或者加在一个具体的类型前。一个 final 变量只可以被赋值一次;一个 const 变量是一个编译时常量(const 变量同时也是 final 的)。顶层的 final 变量或者类的 final 变量在其第一次使用的时候被初始化。

 备忘:

实例变量可以是 final 的但不可以是 const 的, final 实例变量必须在构造器开始前被初始化,比如在声明实例变量时初始化,或者作为构造器参数,或者将其置于构造器的 初始化列表中。

下面的示例中我们创建并设置两个 final 变量:

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

你不能修改一个 final 变量的值:

name = 'Alice'; // Error: a final variable can only be set once.

使用关键字 const 修饰变量表示该变量为 编译时常量。如果使用 const 修饰类中的变量,则必须加上 static 关键字,即 static const(译者注:顺序不能颠倒)。在声明 const 变量时可以直接为其赋值,也可以使用其它的 const 变量为其赋值:

const bar = 1000000; // 直接赋值 [Unit of pressure (dynes/cm2)]
const double atm = 1.01325 * bar; // 利用其它 const 变量赋值 (Standard atmosphere)

const 关键字不仅仅可以用来定义常量,还可以用来创建 常量值,该常量值可以赋予给任何变量。你也可以将构造函数声明为 const 的,这种类型的构造函数创建的对象是不可改变的。

var foo = const [];
final bar = const [];
const baz = []; // 相当于 `const []` (Equivalent to `const []`)

如果使用初始化表达式为常量赋值可以省略掉关键字 const,比如上面的常量 baz 的赋值就省略掉了 const。详情请查阅 不要冗余地使用 const

没有使用 final 或 const 修饰的变量的值是可以被更改的,即使这些变量之前引用过 const 的值。

foo = [1, 2, 3]; // foo 的值之前为 const [] (Was const [])

常量的值不可以被修改:

baz = [42]; // 报错:常量不可以被赋值。(Error: Constant variables can't be assigned a value.)

你可以在常量中使用 类型检查和强制类型转换 (is 和 as)、 集合中的 if 以及 展开操作符 (... 和 ...?):

const Object i = 3; // Where i is a const Object with an int value...
const list = [i as int]; // Use a typecast.
const map = {if (i is int) i: "int"}; // Use is and collection if.
const set = {if (list is List<int>) ...list}; // ...and a spread.

 备忘: Although a final object cannot be modified, its fields can be changed. In comparison, a const object and its fields cannot be changed: they’re immutable.

可以查阅 ListsMaps 和 Classes 获取更多关于使用 const 创建常量值的信息。

内置类型

Dart 语言支持下列的类型:

  • numbers
  • strings
  • booleans
  • lists (也被称为 arrays)

  • sets
  • maps
  • runes (用于在字符串中表示 Unicode 字符)

  • symbols

可以直接使用字面量来初始化上述类型。例如 'This is a string' 是一个字符串字面量,true 是一个布尔字面量。

由于 Dart 中每个变量引用都指向一个对象(一个  的实例),你通常也可以使用 构造器 来初始化变量。一些内置的类型有它们自己的构造器。例如你可以使用 Map() 来创建一个 map 对象。

Numbers

Dart 支持两种 Number 类型:

int

整数值;长度不超过 64 位,具体取值范围依赖于不同的平台。在 DartVM 上其取值位于 -263 至 263 - 1 之间。编译成 JavaScript 的 Dart 使用 JavaScript 数字,其允许的取值范围在 -253 至 253 - 1 之间。

double

64 位的双精度浮点数字,且符合 IEEE 754 标准。

int 和 double 都是 num 的子类。 num 中定义了一些基本的运算符比如 +、-、*、/ 等,还定义了 abs()ceil() 和 floor() 等方法(位运算符,比如 >> 定义在 int 中)。如果 num 及其子类不满足你的要求,可以查看 dart:math 库中的 API。

整数是不带小数点的数字,下面是一些定义整数字面量的例子:

var x = 1;
var hex = 0xDEADBEEF;

如果一个数字包含了小数点,那么它就是浮点型的。下面是一些定义浮点数字面量的例子:

var y = 1.1;
var exponents = 1.42e5;

整型字面量将会在必要的时候自动转换成浮点数字面量:

double z = 1; // Equivalent to double z = 1.0.

 版本提示:

在 Dart 2.1 之前,在浮点数上下文中使用整数字面量是错误的。

下面是字符串和数字之间转换的方式:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

整型支持传统的位移操作,比如移位(<<、>>)、按位与(&)、按位或(|),例如:

assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 >> 1) == 1); // 0011 >> 1 == 0001
assert((3 | 4) == 7); // 0011 | 0100 == 0111

数字字面量为编译时常量。很多算术表达式只要其操作数是常量,则表达式结果也是编译时常量。

const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;

Strings

Dart 字符串是 UTF-16 编码的字符序列。可以使用单引号或者双引号来创建字符串:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

// 代码中文解释
var s1 = '使用单引号创建字符串字面量。';
var s2 = "双引号也可以用于创建字符串字面量。";
var s3 = '使用单引号创建字符串时可以使用斜杠来转义那些与单引号冲突的字符串:\'。';
var s4 = "而在双引号中则不需要使用转义与单引号冲突的字符串:'";

在字符串中,请以 ${表达式} 的形式使用表达式,如果表达式是一个标识符,可以省略掉 {}。如果表达式的结果为一个对象,则 Dart 会调用该对象的 toString 方法来获取一个字符串。

var s = 'string interpolation';

assert('Dart has $s, which is very handy.' ==
    'Dart has string interpolation, ' +
        'which is very handy.');
assert('That deserves all caps. ' +
        '${s.toUpperCase()} is very handy!' ==
    'That deserves all caps. ' +
        'STRING INTERPOLATION is very handy!');

// 代码中文解释
var s = '字符串插值';

assert('Dart 有$s,使用起来非常方便。' == 'Dart 有字符串插值,使用起来非常方便。');
assert('使用${s.substring(3,5)}表达式也非常方便' == '使用插值表达式也非常方便。');

 备忘:

== 运算符负责判断两个对象的内容是否一样,如果两个字符串包含一样的字符编码序列,则表示相等。

你可以使用 + 运算符或并列放置多个字符串来连接字符串:

var s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 ==
    'String concatenation works even over '
        'line breaks.');

var s2 = 'The + operator ' + 'works, as well.';
assert(s2 == 'The + operator works, as well.');

// 代码中文解释
var s1 = '可以拼接'
    '字符串'
    "即便它们不在同一行。";
assert(s1 == '可以拼接字符串即便它们不在同一行。');

var s2 = '使用加号 + 运算符' + '也可以达到相同的效果。';
assert(s2 == '使用加号 + 运算符也可以达到相同的效果。');

使用三个单引号或者三个双引号也能创建多行字符串:

var s1 = '''
你可以像这样创建多行字符串。
''';

var s2 = """这也是一个多行字符串。""";

在字符串前加上 r 作为前缀创建 “raw” 字符串(即不会被做任何处理(比如转义)的字符串):

var s = r'In a raw string, not even \n gets special treatment.';

// 代码中文解释
var s = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。';

你可以查阅 Runes 与 grapheme clusters 获取更多关于如何在字符串中表示 Unicode 字符的信息。

字符串字面量是一个编译时常量,只要是编译时常量都可以作为字符串字面量的插值表达式:

// 可以将下面三个常量作为字符串插值拼接到字符串字面量中。(These work in a const string.)
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// 而下面三个常量不能作为字符串插值拼接到字符串字面量。
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [1, 2, 3];

const validConstString = '$aConstNum $aConstBool $aConstString';
// const invalidConstString = '$aNum $aBool $aString $aConstList';

可以查阅 字符串和正则表达式 获取更多关于如何使用字符串的信息。

Booleans

Dart 使用 bool 关键字表示布尔类型,布尔类型只有两个对象 true 和 false,两者都是编译时常量。

Dart 的类型安全不允许你使用类似 if (nonbooleanValue) 或者 assert (nonbooleanValue) 这样的代码检查布尔值。相反,你应该总是显示地检查布尔值,比如像下面的代码这样:

// 检查是否为空字符串 (Check for an empty string).
var fullName = '';
assert(fullName.isEmpty);

// 检查是否小于等于零。
var hitPoints = 0;
assert(hitPoints <= 0);

// 检查是否为 null。
var unicorn;
assert(unicorn == null);

// 检查是否为 NaN。
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

Lists

数组(Array)是几乎所有编程语言中最常见的集合类型,在 Dart 中数组由 List 对象表示。通常称之为 List

Dart 中 List 字面量看起来与 JavaScript 中数组字面量一样。下面是一个 Dart List 的示例:

var list = [1, 2, 3];

 备忘:

这里 Dart 推断出 list 的类型为 List<int>,如果往该数组中添加一个非 int 类型的对象则会报错。你可以阅读 类型推断 获取更多相关信息。

你可以在 Dart 的集合类型的最后一个项目后添加逗号。这个尾随逗号并不会影响集合,但它能有效避免「复制粘贴」的错误。

var list = [
  'Car',
  'Boat',
  'Plane',
];

List 的下标索引从 0 开始,第一个元素的下标为 0,最后一个元素的下标为 list.length - 1。你可以像 JavaScript 中的用法那样获取 Dart 中 List 的长度以及元素:

var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);

在 List 字面量前添加 const 关键字会创建一个编译时常量:

var constantList = const [1, 2, 3];
// constantList[1] = 1; // This line will cause an error.

Dart 在 2.3 引入了 扩展操作符...)和 空感知扩展操作符...?),它们提供了一种将多个元素插入集合的简洁方法。

例如,你可以使用扩展操作符(...)将一个 List 中的所有元素插入到另一个 List 中:

var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);

如果扩展操作符右边可能为 null ,你可以使用 null-aware 扩展操作符(...?)来避免产生异常:

var list;
var list2 = [0, ...?list];
assert(list2.length == 1);

可以查阅扩展操作符建议获取更多关于如何使用扩展操作符的信息。

Dart 还同时引入了 集合中的 if 和 集合中的 for 操作,在构建集合时,可以使用条件判断 (if) 和循环 (for)。

下面示例是使用 集合中的 if 来创建一个 List 的示例,它可能包含 3 个或 4 个元素:

var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];

下面是使用 集合中的 for 将列表中的元素修改后添加到另一个列表中的示例:

var listOfInts = [1, 2, 3];
var listOfStrings = [
  '#0',
  for (var i in listOfInts) '#$i'
];
assert(listOfStrings[1] == '#1');

你可以查阅 集合中使用控制流建议 获取更多关于在集合中使用 if 和 for 的细节内容和示例。

List 类中有许多用于操作 List 的便捷方法,你可以查阅 泛型 和 集合 获取更多与之相关的信息。

Sets

在 Dart 中,set 是一组特定元素的无序集合。 Dart 支持的 set 由 set literals 和 Set 类提供。

 版本提示:

尽管 Set 类型(type) 一直都是 Dart 的一项核心功能,但是 Set 字面量(literals) 是在 Dart 2.2 中才加入的。

下面是使用 Set 字面量来创建一个 Set 集合的方法:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

 备忘:

Dart 推断 halogens 变量是一个 Set<String> 类型的集合,如果往该 Set 中添加类型不正确的对象则会报错。你可以查阅 类型推断 获取更多与之相关的内容。

可以使用在 {} 前加上类型参数的方式创建一个空的 Set,或者将 {} 赋值给一个 Set 类型的变量:

var names = <String>{}; // 类型+{}的形式创建Set。
// Set<String> names = {}; // 声明类型变量的形式创建 Set (This works, too).
// var names = {}; // 这样的形式将创建一个 Map 而不是 Set (Creates a map, not a set.)

 

Set 还是 map? Map 字面量语法相似于 Set 字面量语法。因为先有的 Map 字面量语法,所以 {} 默认是 Map 类型。如果忘记在 {} 上注释类型或赋值到一个未声明类型的变量上,那么 Dart 会创建一个类型为 Map<dynamic, dynamic> 的对象。

使用 add() 方法或 addAll() 方法向已存在的 Set 中添加项目:

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);

使用 .length 可以获取 Set 中元素的数量:

var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);

可以在 Set 字面量前添加 const 关键字创建一个 Set 编译时常量:

final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // This line will cause an error.

从 Dart 2.3 开始,Set 可以像 List 一样支持使用扩展操作符(... 和 ...?)以及 Collection If 和 Collection For 操作。你可以查阅 List 扩展操作符 和 List 集合操作符 获取更多相关信息。

你也可以查阅 泛型 以及 Set 获取更多相关信息。

Maps

通常来说,Map 是用来关联 keys 和 values 的对象。其中键和值都可以是任何类型的对象。每个  只能出现一次但是  可以重复出现多次。 Dart 中 Map 提供了 Map 字面量以及 Map 类型两种形式的 Map。

下面是一对使用 Map 字面量创建 Map 的例子:

var gifts = {
  // 键:    值
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

 备忘:

Dart 将 gifts 变量的类型推断为 Map<String, String>,而将 nobleGases 的类型推断为 Map<int, String>。如果你向这两个 Map 对象中添加不正确的类型值,将导致运行时异常。你可以阅读 类型推断 获取更多相关信息。

你也可以使用 Map 的构造器创建 Map:

var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

 备忘: If you come from a language like C# or Java, you might expect to see new Map() instead of just Map(). In Dart, the new keyword is optional. For details, see Using constructors.

如果你之前是使用的 C# 或 Java 这样的语言,也许你想使用 new Map() 构造 Map 对象。但是在 Dart 中,new 关键词是可选的。(译者注:且不被建议使用) 你可以查阅 构造函数的使用 获取更多相关信息。

向现有的 Map 中添加键值对与 JavaScript 的操作类似:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // 添加键值对 (Add a key-value pair)

从一个 Map 中获取一个值的操作也与 JavaScript 类似。

var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');

如果检索的 Key 不存在于 Map 中则会返回一个 null:

var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);

使用 .length 可以获取 Map 中键值对的数量:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

在一个 Map 字面量前添加 const 关键字可以创建一个 Map 编译时常量:

final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

// constantMap[2] = 'Helium'; // This line will cause an error.

Map 可以像 List 一样支持使用扩展操作符(... 和 ...?)以及集合的 if 和 for 操作。你可以查阅 List 扩展操作符 和 List 集合操作符 获取更多相关信息。

你也可以查阅 泛型 以及 Maps 获取更多相关信息。

Runes 与 grapheme clusters

在 Dart 中,runes 公开了字符串的 Unicode 码位。使用 characters 包 来访问或者操作用户感知的字符,也被称为 Unicode (扩展) grapheme clusters

Unicode 编码为每一个字母、数字和符号都定义了一个唯一的数值。因为 Dart 中的字符串是一个 UTF-16 的字符序列,所以如果想要表示 32 位的 Unicode 数值则需要一种特殊的语法。

表示 Unicode 字符的常见方式是使用 \uXXXX,其中 XXXX 是一个四位数的 16 进制数字。例如心形字符(♥)的 Unicode 为 \u2665。对于不是四位数的 16 进制数字,需要使用大括号将其括起来。例如大笑的 emoji 表情(😆)的 Unicode 为 \u{1f600}

如果你需要读写单个 Unicode 字符,可以使用 characters 包中定义的 characters getter。它将返回 Characters 对象作为一系列 grapheme clusters 的字符串。下面是使用 characters API 的样例:

import 'package:characters/characters.dart';
...
var hi = 'Hi 🇩🇰';
print(hi);
print('The end of the string: ${hi.substring(hi.length - 1)}');
print('The last character: ${hi.characters.last}\n');

输出取决于你的环境,大致类似于:

$ dart bin/main.dart
Hi 🇩🇰
The end of the string: ???
The last character: 🇩🇰

有关使用 characters 包操作字符串的详细信息,请参阅用于 characters 包的样例 和 API 参考

 备忘:

在使用 List 操作 Rune 的时候需要小心,根据所操作的语种、字符集等不同可能会导致字符串出现问题,具体可参考 Stack Overflow 中的提问: [我如何在 Dart 中反转一个字符串?][How do I reverse a String in Dart?]。

Symbols

Symbol 表示 Dart 中声明的操作符或者标识符。你几乎不会需要 Symbol,但是它们对于那些通过名称引用标识符的 API 很有用,因为代码压缩后,尽管标识符的名称会改变,但是它们的 Symbol 会保持不变。

可以使用在标识符前加 # 前缀来获取 Symbol:

#radix
#bar

Symbol 字面量是编译时常量。

函数

Dart 是一种真正面向对象的语言,所以即便函数也是对象并且类型为 Function,这意味着函数可以被赋值给变量或者作为其它函数的参数。你也可以像调用函数一样调用 Dart 类的实例。详情请查阅 可调用的类

下面是定义一个函数的例子:

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

虽然高效 Dart 指南建议在公开的 API 上定义返回类型,不过即便不定义,该函数也依然有效:

isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

如果函数体内只包含一个表达式,你可以使用简写语法:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

语法 => 表达式 是 { return 表达式; } 的简写, => 有时也称之为 箭头 函数。

 备忘:

在 => 与 ; 之间的只能是 表达式 而非 语句。比如你不能将一个 if语句 放在其中,但是可以放置 条件表达式

参数

函数可以有两种形式的参数:必要参数 和 可选参数。必要参数定义在参数列表前面,可选参数则定义在必要参数后面。可选参数可以是 命名的 或 位置的

 备忘:

某些 API(特别是 Flutter 控件的构造器)只使用命名参数,即便参数是强制性的。可以查阅下一节获取更多信息。

向函数传入参数或者定义函数参数时,可以使用 [尾随逗号][trailing comma]。

命名参数

命名参数默认为可选参数,除非他们被特别标记为必要的。

当你调用函数时,可以使用 参数名参数值 的形式来指定命名参数。例如:

enableFlags(bold: true, hidden: false);

When defining a function, use {参数1参数2, …} to specify named parameters:

定义函数时,使用 {param1param2, …} 来指定命名参数:

/// 设置 [bold] 和 [hidden] 标识……
/// Sets the [bold] and [hidden] flags...
void enableFlags({bool bold, bool hidden}) {...}

虽然命名参数是可选参数的一种类型,但是你仍然可以使用 @required 注解来标识一个命名参数是必须的参数,此时调用者必须为该参数提供一个值。例如:

const Scrollbar({Key key, @required Widget child})

如果调用者想要通过 Scrollbar 的构造函数构造一个 Scrollbar 对象而不提供 child 参数,则会导致编译错误。

@required 注解定义在 meta package 中,可以通过导入 package:meta/meta.dart 包使用。

可选的位置参数

使用 [] 将一系列参数包裹起来作为位置参数:

String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

下面是不使用可选参数调用上述函数的示例:

assert(say('Bob', 'Howdy') == 'Bob says Howdy');

下面是使用可选参数调用上述函数的示例:

assert(say('Bob', 'Howdy', 'smoke signal') ==
    'Bob says Howdy with a smoke signal');

默认参数值

可以用 = 为函数的命名参数和位置参数定义默认值,默认值必须为编译时常量,没有指定默认值的情况下默认值为 null

下面是设置可选参数默认值示例:

/// 设置 [bold] 和 [hidden] 标识……
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold 的值将为 true;而 hidden 将为 false。
enableFlags(bold: true);

 

在老版本的 Dart 代码中会使用冒号(:)而不是 = 来设置命名参数的默认值。原因在于刚开始的时候命名参数只支持 :。不过现在这个支持已经过时,所以我们建议你现在仅 使用 = 来指定默认值

下一个示例将向你展示如何为位置参数设置默认值:

String say(String from, String msg,
    [String device = 'carrier pigeon']) {
  var result = '$from says $msg with a $device';
  return result;
}

assert(say('Bob', 'Howdy') ==
    'Bob says Howdy with a carrier pigeon');

List 或 Map 同样也可以作为默认值。下面的示例定义了一个名为 doStuff() 的函数,并为其名为 list 和 gifts 的参数指定了一个 List 类型的值和 Map 类型的值。

void doStuff(
    {List<int> list = const [1, 2, 3],
    Map<String, String> gifts = const {
      'first': 'paper',
      'second': 'cotton',
      'third': 'leather'
    }}) {
  print('list:  $list');
  print('gifts: $gifts');
}

main() 函数

每个 Dart 程序都必须有一个 main() 顶级函数作为程序的入口,main() 函数返回值为 void 并且有一个 List<String> 类型的可选参数。

下面是一个 Web 应用的 main() 函数示例:

void main() {
  querySelector('#sample_text_id')
    ..text = 'Click me!'
    ..onClick.listen(reverseText);
}

 备忘:

上述代码中的 .. 语法称之为 级联调用。使用级联访问可以在一个对象上执行多个操作。

下面是使用命令行访问带参数的 main() 函数示例:

// 使用命令 dart args.dart 1 test 运行该应用
// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}

你可以通过使用 参数库 来定义和解析命令行参数。

函数是一级对象

可以将函数作为参数传递给另一个函数。例如:

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// 将 printElement 函数作为参数传递。
list.forEach(printElement);

你也可以将函数赋值给一个变量,比如:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

该示例中使用了匿名函数。下一节会有更多与其相关的介绍。

匿名函数

大多数方法都是有名字的,比如 main() 或 printElement()。你可以创建一个没有名字的方法,称之为 匿名函数、 Lambda 表达式 或 Closure 闭包。你可以将匿名方法赋值给一个变量然后使用它,比如将该变量添加到集合或从中删除。

匿名方法看起来与命名方法类似,在括号之间可以定义参数,参数之间用逗号分割。

后面大括号中的内容则为函数体:

([[类型参数[, …]]) {
  函数体;
};

下面代码定义了只有一个参数 item 且没有参数类型的匿名方法。 List 中的每个元素都会调用这个函数,打印元素位置和值的字符串:

var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
  print('${list.indexOf(item)}: $item');
});

点击运行按钮执行代码。

如果函数体内只有一行返回语句,你可以使用胖箭头缩写法。粘贴下面代码到 DartPad 中并点击运行按钮,验证两个函数是否一致。

list.forEach(
    (item) => print('${list.indexOf(item)}: $item'));

词法作用域

Dart 是词法有作用域语言,变量的作用域在写代码的时候就确定了,大括号内定义的变量只能在大括号内访问,与 Java 类似。

下面是一个嵌套函数中变量在多个作用域中的示例:

bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

注意 nestedFunction() 函数可以访问包括顶层变量在内的所有的变量。

词法闭包

闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。

函数可以封闭定义到它作用域内的变量。接下来的示例中,函数 makeAdder() 捕获了变量 addBy。无论函数在什么时候返回,它都可以使用捕获的 addBy 变量。

/// 返回一个将 [addBy] 添加到该函数参数的函数。
/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // 生成加 2 的函数。
  var add2 = makeAdder(2);

  // 生成加 4 的函数。
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}

测试函数是否相等

下面是顶级函数,静态方法和示例方法相等性的测试示例:

void foo() {} // 定义顶层函数 (A top-level function)

class A {
  static void bar() {} // 定义静态方法
  void baz() {} // 定义实例方法
}

void main() {
  var x;

  // 比较顶层函数是否相等。
  x = foo;
  assert(foo == x);

  // 比较静态方法是否相等。
  x = A.bar;
  assert(A.bar == x);

  // 比较实例方法是否相等。
  var v = A(); // A 的实例 #1
  var w = A(); // A 的实例 #2
  var y = w;
  x = w.baz;

  // 这两个闭包引用了相同的实例对象,因此它们相等。
  assert(y.baz == x);

  // 这两个闭包引用了不同的实例对象,因此它们不相等。
  assert(v.baz != w.baz);
}

返回值

所有的函数都有返回值。没有显示返回语句的函数最后一行默认为执行 return null;

foo() {}

assert(foo() == null);

运算符

Dart 支持下表的操作符。你可以将这些运算符实现为 一个类的成员

描述

运算符

一元后缀

表达式++ 表达式-- () [] . ?.

一元前缀

-表达式 !表达式 ~表达式 ++表达式 --表达式

乘除法

* / % ~/

加减法

+ -

位运算

<< >> >>>

二进制与

&

二进制异或

^

二进制或

|

关系和类型测试

>= > <= < as is is!

相等判断

== !=

逻辑与

&&

逻辑或

||

空判断

??

条件表达式

表达式 1 ? 表达式 2 : 表达式 3

级联

..

赋值

= *= /= += -= &= ^= 等等……

 请注意:

上述运算符优先级是对 Dart 解析器行为的效仿。更准确的描述,请参阅 Dart 语言规范 中的语法。

一旦你使用了运算符,就创建了表达式。下面是一些运算符表达式的示例:

a++
a + b
a = b
a == b
c ? a : b
a is T

运算符表 中,运算符的优先级按先后排列,即第一行优先级最高,最后一行优先级最低,而同一行中,最左边的优先级最高,最右边的优先级最低。例如:% 运算符优先级高于 == ,而 == 高于 &&。根据优先级规则,那么意味着以下两行代码执行的效果相同:

// 括号提高了可读性。
// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) ...

// 难以理解,但是与上面的代码效果一样。
if (n % i == 0 && d % i == 0) ...

 请注意:

对于有两个操作数的运算符,左边的操作数决定了运算符的功能。比如对于一个 Vector 对象和一个 Point 对象,表达式 aVector + aPoint 中所使用的是 Vector 对象中定义的相加运算符 (+)。

算术运算符

Dart 支持常用的算术运算符:

运算符描述
+
-表达式一元负, 也可以作为反转(反转表达式的符号)
*
/
~/除并取整
%取模

示例:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // 结果是一个浮点数
assert(5 ~/ 2 == 2); // 结果是一个整数
assert(5 % 2 == 1); // 取余

assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 还支持自增自减操作。

Operator

++var

var = var + 1 (表达式的值为 var + 1)

var++var = var + 1 (表达式的值为 var)
--varvar = var – 1 (表达式的值为 var – 1)
var--var = var – 1 (表达式的值为 var)

示例:

var a, b;

a = 0;
b = ++a; // 在 b 赋值前将 a 增加 1。
assert(a == b); // 1 == 1

a = 0;
b = a++; // 在 b 赋值后将 a 增加 1。
assert(a != b); // 1 != 0

a = 0;
b = --a; // 在 b 赋值前将 a 减少 1。
assert(a == b); // -1 == -1

a = 0;
b = a--; // 在 b 赋值后将 a 减少 1。
assert(a != b); // -1 != 0

关系运算符

下表列出了关系运算符及含义:

Operator

==

相等

!=不等
>大于
<小于
>=大于等于
<=小于等于

要判断两个对象 x 和 y 是否表示相同的事物使用 == 即可。(在极少数情况下,可能需要使用 identical() 函数来确定两个对象是否完全相同。)。下面是 == 运算符的一些规则:

  1. 假设有变量 x 和 y,且 x 和 y 至少有一个为 null,则当且仅当 x 和 y 均为 null 时 x == y 才会返回 true,否则只有一个为 null 则返回 false。

  2. x.==(y) 将会返回值,这里不管有没有 y,即 y 是可选的。也就是说 == 其实是 x 中的一个方法,并且可以被重写。详情请查阅重写运算符

下面的代码给出了每一种关系运算符的示例:

assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);

类型判断运算符

asisis! 运算符是在运行时判断对象类型的运算符。

OperatorMeaning
as

类型转换(也用作指定 类前缀))

is

如果对象是指定类型则返回 true

is!

如果对象是指定类型则返回 false

当且仅当 obj 实现了 T 的接口,obj is T 才是 true。例如 obj is Object 总为 true,因为所有类都是 Object 的子类。

仅当你确定这个对象是该类型的时候,你才可以使用 as 操作符可以把对象转换为特定的类型。例如:

(emp as Person).firstName = 'Bob';

如果你不确定这个对象类型是不是 T,请在转型前使用 is T 检查类型。

if (emp is Person) {
  // 类型检查
  emp.firstName = 'Bob';
}

你可以使用 as 运算符进行缩写:

(emp as Person).firstName = 'Bob';

 备忘:

上述两种方式是有区别的:如果 emp 为 null 或者不为 Person 类型,则第一种方式将会抛出异常,而第二种不会。

赋值运算符

可以使用 = 来赋值,同时也可以使用 ??= 来为值为 null 的变量赋值。

// 将 value 赋值给 a (Assign value to a)
a = value;
// 当且仅当 b 为 null 时才赋值
b ??= value;

像 += 这样的赋值运算符将算数运算符和赋值运算符组合在了一起。

=–=/=%=>>=^=
+=*=~/=<<=&=|=

下表解释了符合运算符的原理:

场景复合运算等效表达式
假设有运算符 opop= ba = a op b
示例:a += ba = a + b

下面的例子展示了如何使用赋值以及复合赋值运算符:

var a = 2; // 使用 = 赋值 (Assign using =)
a *= 3; // 赋值并做乘法运算 Assign and multiply: a = a * 3
assert(a == 6);

逻辑运算符

使用逻辑运算符你可以反转或组合布尔表达式。

运算符描述
!表达式对表达式结果取反(即将 true 变为 false,false 变为 true)
||逻辑或
&&逻辑与

下面是使用逻辑表达式的示例:

if (!done && (col == 0 || col == 3)) {
  // ...Do something...
}

按位和移位运算符

在 Dart 中,二进制位运算符可以操作二进制的某一位,但仅适用于整数。

运算符描述
&按位与
|按位或
^按位异或
~表达式按位取反(即将 “0” 变为 “1”,“1” 变为 “0”)
<<位左移
>>位右移

下面是使用按位和移位运算符的示例:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // 按位与 (AND)
assert((value & ~bitmask) == 0x20); // 取反后按位与 (AND NOT)
assert((value | bitmask) == 0x2f); // 按位或 (OR)
assert((value ^ bitmask) == 0x2d); // 按位异或 (XOR)
assert((value << 4) == 0x220); // 位左移 (Shift left)
assert((value >> 4) == 0x02); // 位右移 (Shift right)

条件表达式

Dart 有两个特殊的运算符可以用来替代 if-else 语句:

condition ? expr1 : expr2

If condition is true, evaluates expr1 (and returns its value); otherwise, evaluates and returns the value of expr2.

条件 ? 表达式 1 : 表达式 2 :如果条件为 true,执行表达式 1并返回执行结果,否则执行表达式 2 并返回执行结果。

expr1 ?? expr2

If expr1 is non-null, returns its value; otherwise, evaluates and returns the value of expr2.

表达式 1 ?? 表达式 2:如果表达式 1 为非 null 则返回其值,否则执行表达式 2 并返回其值。

根据布尔表达式确定赋值时,请考虑使用 ?:

var visibility = isPublic ? 'public' : 'private';

如果赋值是根据判定是否为 null 则考虑使用 ??

String playerName(String name) => name ?? 'Guest';

上述示例还可以写成至少下面两种不同的形式,只是不够简洁:

// 相对使用 ?: 运算符来说稍微长了点。(Slightly longer version uses ?: operator).
String playerName(String name) => name != null ? name : 'Guest';

// 如果使用 if-else 则更长。
String playerName(String name) {
  if (name != null) {
    return name;
  } else {
    return 'Guest';
  }
}

级联运算符(..)

级联运算符(..)可以让你在同一个对象上连续调用多个对象的变量或方法。

比如下面的代码:

querySelector('#confirm') // 获取对象 (Get an object).
  ..text = 'Confirm' // 使用对象的成员 (Use its members).
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

第一个方法 querySelector 返回了一个 Selector 对象,后面的级联操作符都是调用这个 Selector 对象的成员并忽略每个操作的返回值。

上面的代码相当于:

var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));

级联运算符可以嵌套,例如:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

在返回对象的函数中谨慎使用级联操作符。例如,下面的代码是错误的:

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // 出错:void 对象中没有方法 write (Error: method 'write' isn't defined for 'void').

上述代码中的 sb.write() 方法返回的是 void,返回值为 void 的方法则不能使用级联运算符。

 备忘:

严格来说 .. 级联操作并非一个运算符而是 Dart 的特殊语法。

其他运算符

大多数其它的运算符,已经在其它的示例中使用过:

运算符名字描述
()使用方法代表调用一个方法
[]访问 List访问 List 中特定位置的元素
.访问成员成员访问符
?.条件访问成员与上述成员访问符类似,但是左边的操作对象不能为 null,例如 foo?.bar,如果 foo 为 null 则返回 null ,否则返回 bar

更多关于 .?. 和 .. 运算符介绍,请参考.

流程控制语句

你可以使用下面的语句来控制 Dart 代码的执行流程:

  • if 和 else

  • for 循环

  • while 和 do-while 循环

  • break 和 continue

  • switch 和 case

  • assert

使用 try-catch 和 throw 也能影响控制流,详情参考异常部分。

If 和 Else

Dart 支持 if - else 语句,其中 else 是可选的,比如下面的例子。你也可以参考条件表达式

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

不同于 JavaScript,Dart 的 if 语句中的条件必须是布尔值而不能为其它类型。详情请查阅布尔值

For 循环

你可以使用标准的 for 循环进行迭代。例如:

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
  message.write('!');
}

在 Dart 语言中,for 循环中的闭包会自动捕获循环的 索引值 以避免 JavaScript 中一些常见的陷阱。假设有如下代码:

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());

上述代码执行后会输出 0 和 1,但是如果在 JavaScript 中执行同样的代码则会输出两个 2

如果要遍历的对象实现了 Iterable 接口,则可以使用 forEach() 方法,如果不需要使用到索引,则使用 forEach 方法是一个非常好的选择:

candidates.forEach((candidate) => candidate.interview());

像 List 和 Set 等实现了 Iterable 接口的类还支持 for-in 形式的 迭代

var collection = [1, 2, 3];
for (var x in collection) {
  print(x); // 1 2 3
}

While 和 Do-While

while 循环会在执行循环体前先判断条件:

while (!isDone()) {
  doSomething();
}

do-while 循环则会 先执行一遍循环体 再判断条件:

do {
  printLine();
} while (!atEndOfPage());

Break 和 Continue

使用 break 可以中断循环:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

使用 continue 可以跳过本次循环直接进入下一次循环:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

如果你正在使用诸如 List 或 Set 之类的 Iterable 对象,你可以用以下方式重写上述例子:

candidates
    .where((c) => c.yearsExperience >= 5)
    .forEach((c) => c.interview());

Switch 和 Case

Switch 语句在 Dart 中使用 == 来比较整数、字符串或编译时常量,比较的两个对象必须是同一个类型且不能是子类并且没有重写 == 操作符。 枚举类型非常适合在 Switch 语句中使用。

 备忘:

Dart 中的 Switch 语句仅适用于有限的情况,比如使用解释器和扫描器的场景。

每一个非空的 case 子句都必须有一个 break 语句,也可以通过 continuethrow 或者 return 来结束非空 case 语句。

不匹配任何 case 语句的情况下,会执行 default 子句中的代码:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

下面的例子忽略了 case 子句的 break 语句,因此会产生错误:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // 错误: 没有 break

  case 'CLOSED':
    executeClosed();
    break;
}

但是,Dart 支持空的 case 语句,允许其以 fall-through 的形式执行。

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // case 语句为空时的 fall-through 形式。
  case 'NOW_CLOSED':
    // case 条件值为 CLOSED 和 NOW_CLOSED 时均会执行该语句。
    executeNowClosed();
    break;
}

在非空 case 语句中想要实现 fall-through 的形式,可以使用 continue 语句配合 label 的方式实现:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // 继续执行标签为 nowClosed 的 case 子句。

  nowClosed:
  case 'NOW_CLOSED':
    // case 条件值为 CLOSED 和 NOW_CLOSED 时均会执行该语句。
    executeNowClosed();
    break;
}

每个 case 子句都可以有局部变量且仅在该 case 语句内可见。

断言

在开发过程中,可以在条件表达式为 false 时使用 — assert(条件可选信息); — 语句来打断代码的执行。你可以在本文中找到大量使用 assert 的例子。下面是相关示例:

// 确保变量值不为 null (Make sure the variable has a non-null value)
assert(text != null);

// 确保变量值小于 100。
assert(number < 100);

// 确保这是一个 https 地址。
assert(urlString.startsWith('https'));

assert 的第二个参数可以为其添加一个字符串消息。

assert(urlString.startsWith('https'),
    'URL ($urlString) should start with "https".');

assert 的第一个参数可以是值为布尔值的任何表达式。如果表达式的值为 true,则断言成功,继续执行。如果表达式的值为 false,则断言失败,抛出一个 AssertionError 异常。

如何判断 assert 是否生效?assert 是否生效依赖开发工具和使用的框架:

  • Flutter 在调试模式时生效。

  • 一些开发工具比如 dartdevc 通常情况下是默认生效的。

  • 其他一些工具,比如 dart 以及 dart2js 通过在运行 Dart 程序时添加命令行参数 --enable-asserts 使 assert 生效。

在生产环境代码中,断言会被忽略,与此同时传入 assert 的参数不被判断。

异常

Dart 代码可以抛出和捕获异常。异常表示一些未知的错误情况,如果异常没有捕获则会被抛出从而导致抛出异常的代码终止执行。

与 Java 不同的是,Dart 的所有异常都是非必检异常,方法不必声明会抛出哪些异常,并且你也不必捕获任何异常。

Dart 提供了 Exception 和 Error 两种类型的异常以及它们一系列的子类,你也可以定义自己的异常类型。但是在 Dart 中可以将任何非 null 对象作为异常抛出而不局限于 Exception 或 Error 类型。

抛出异常

下面是关于抛出或者 引发 异常的示例:

throw FormatException('Expected at least 1 section');

你也可以抛出任意的对象:

throw 'Out of llamas!';

 备忘:

优秀的代码通常会抛出 Error 或 Exception 类型的异常。

因为抛出异常是一个表达式,所以可以在 => 语句中使用,也可以在其他使用表达式的地方抛出异常:

void distanceTo(Point other) => throw UnimplementedError();

捕获异常

捕获异常可以避免异常继续传递(重新抛出异常除外)。捕获一个异常可以给你处理它的机会:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

对于可以抛出多种异常类型的代码,也可以指定多个 catch 语句,每个语句分别对应一个异常类型,如果 catch 语句没有指定异常类型则表示可以捕获任意异常类型:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // 指定异常
  buyMoreLlamas();
} on Exception catch (e) {
  // 其它类型的异常
  print('Unknown exception: $e');
} catch (e) {
  // // 不指定类型,处理其它全部
  print('Something really unknown: $e');
}

如上述代码所示可以使用 on 或 catch 来捕获异常,使用 on 来指定异常类型,使用 catch 来捕获异常对象,两者可同时使用。

你可以为 catch 方法指定两个参数,第一个参数为抛出的异常对象,第二个参数为栈信息 StackTrace 对象:

try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

关键字 rethrow 可以将捕获的异常再次抛出:

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // 运行时错误
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // 允许调用者查看异常。
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}

Finally

无论是否抛出异常,finally 语句始终执行,如果没有指定 catch 语句来捕获异常,则异常会在执行完 finally 语句后抛出:

try {
  breedMoreLlamas();
} finally {
  // 总是清理,即便抛出了异常。
  cleanLlamaStalls();
}

finally 语句会在任何匹配的 catch 语句后执行:

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // 先处理异常。
} finally {
  cleanLlamaStalls(); // 然后清理。
}

你可以阅读 Dart 核心库概览的 异常 章节获取更多相关信息。

Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而所有的类都继承自 Object 类。 基于 mixin 的继承 意味着尽管每个类(Object 除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 Extension 方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。

使用类的成员

对象的 成员 由函数和数据(即 方法 和 实例变量)组成。方法的 调用 要通过对象来完成,这种方式可以访问对象的函数和数据。

使用(.)来访问对象的实例变量或方法:

var p = Point(2, 2);

// 获取 y 值
assert(p.y == 2);

// 调用变量 p 的 distanceTo() 方法。
double distance = p.distanceTo(Point(4, 4));

使用 ?. 代替 . 可以避免因为左边表达式为 null 而导致的问题:

// If p is non-null, set a variable equal to its y value.
var a = p?.y;

使用构造函数

可以使用 构造函数 来创建一个对象。构造函数的命名方式可以为 类名 或 类名 . 标识符 的形式。例如下述代码分别使用 Point() 和 Point.fromJson() 两种构造器创建了 Point 对象:

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

以下代码具有相同的效果,但是构造函数名前面的的 new 关键字是可选的:

var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

 版本提示:

从 Dart 2 开始,new 关键字是可选的。

一些类提供了常量构造函数。使用常量构造函数,在构造函数名之前加 const 关键字,来创建编译时常量时:

var p = const ImmutablePoint(2, 2);

两个使用相同构造函数相同参数值构造的编译时常量是同一个对象:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // 它们是同一个实例 (They are the same instance!)

在 常量上下文 场景中,你可以省略掉构造函数或字面量前的 const 关键字。例如下面的例子中我们创建了一个常量 Map:

// Lots of const keywords here.
// 这里有很多 const 关键字
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

根据上下文,你可以只保留第一个 const 关键字,其余的全部省略:

// Only one const, which establishes the constant context.
// 只需要一个 const 关键字,其它的则会隐式地根据上下文进行关联。
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

但是如果无法根据上下文判断是否可以省略 const,则不能省略掉 const 关键字,否则将会创建一个 非常量对象 例如:

var a = const ImmutablePoint(1, 1); // 创建一个常量 (Creates a constant)
var b = ImmutablePoint(1, 1); // 不会创建一个常量 (Does NOT create a constant)

assert(!identical(a, b)); // 这两变量并不相同 (NOT the same instance!)

 版本提示:

只有从 Dart 2 开始才能根据上下文判断省略 const 关键字。

获取对象的类型

可以使用 Object 对象的 runtimeType 属性在运行时获取一个对象的类型,该对象类型是 Type 的实例。

print('The type of a is ${a.runtimeType}');

到目前为止,我们已经解了如何 使用 类。本节的其余部分将向你介绍如何 实现 一个类。

实例变量

下面是声明实例变量的示例:

class Point {
  double x; // 声明 double 变量 x 并初始化为 null。
  double y; // 声明 double 变量 y 并初始化为 null
  double z = 0; // 声明 double 变量 z 并初始化为 0。
}

所有未初始化的实例变量其值均为 null

所有实例变量均会隐式地声明一个 Getter 方法,非 final 类型的实例变量还会隐式地声明一个 Setter 方法。你可以查阅 Getter 和 Setter 获取更多相关信息。

class Point {
  double x;
  double y;
}

void main() {
  var point = Point();
  point.x = 4; // 使用 x 的 Setter 方法。
  assert(point.x == 4); // 使用 x 的 Getter 方法。
  assert(point.y == null); // 默认值为 null。
}

如果你在声明一个实例变量的时候就将其初始化(而不是在构造函数或其它方法中),那么该实例变量的值就会在对象实例创建的时候被设置,此过程早于执行构造函数以及它的初始化器列表。

构造函数

声明一个与类名一样的函数即可声明一个构造函数(对于命名式构造函数 还可以添加额外的标识符)。大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:

class Point {
  double x, y;

  Point(double x, double y) {
    // 还会有更好的方式来实现此逻辑,敬请期待。
    this.x = x;
    this.y = y;
  }
}

使用 this 关键字引用当前实例。

 备忘:

当且仅当命名冲突时使用 this 关键字才有意义,否则 Dart 会忽略 this 关键字。

对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤:

class Point {
  double x, y;

  // 在构造函数体执行前用于设置 x 和 y 的语法糖。
  Point(this.x, this.y);
}

默认构造函数

如果你没有声明构造函数,那么 Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法。

构造函数不被继承

子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。

命名式构造函数

可以为一个类声明多个命名式构造函数来表达更明确的意图:

class Point {
  double x, y;

  Point(this.x, this.y);

  // 命名式构造函数
  Point.origin()
      : x = 0,
        y = 0;
}

记住构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数,如果你想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。

调用父类非默认构造函数

默认情况下,子类的构造函数会调用父类的匿名无参数构造方法,并且该调用会在子类构造函数的函数体代码执行前,如果子类构造函数还有一个 初始化列表,那么该初始化列表会在调用父类的该构造函数之前被执行,总的来说,这三者的调用顺序如下:

  1. 初始化列表

  2. 父类的无参数构造函数

  3. 当前类的构造函数

如果父类没有匿名无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用(:)指定。

下面的示例中,Employee 类的构造函数调用了父类 Person 的命名构造函数。点击运行按钮执行示例代码。

因为参数会在子类构造函数被执行前传递给父类的构造函数,因此该参数也可以是一个表达式,比如一个函数:

class Employee extends Person {
  Employee() : super.fromJson(defaultData);
  // ···
}

 请注意:

传递给父类构造函数的参数不能使用 this 关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员都不能被访问,但是类成员可以。

初始化列表

除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。每个实例变量之间使用逗号分隔。

// Initializer list sets instance variables before
// the constructor body runs.
// 使用初始化列表在构造函数体执行前设置实例变量。
Point.fromJson(Map<String, double> json)
    : x = json['x'],
      y = json['y'] {
  print('In Point.fromJson(): ($x, $y)');
}

 请注意:

初始化列表表达式 = 右边的语句不能使用 this 关键字。

在开发模式下,你可以在初始化列表中使用 assert 来验证输入数据:

Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

使用初始化列表设置 final 字段非常方便,下面的示例中就使用初始化列表来设置了三个 final 变量的值。点击运行按钮执行示例代码。

重定向构造函数

有时候类中的构造函数仅用于调用类中其它的构造函数,此时该构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数:

class Point {
  double x, y;

  // 该类的主构造函数。
  Point(this.x, this.y);

  // 委托实现给主构造函数。
  Point.alongXAxis(double x) : this(x, 0);
}

常量构造函数

如果类生成的对象都是不变的,可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能。

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

常量构造函数创建的实例并不总是常量,具体可以参考使用构造函数章节。

工厂构造函数

使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。

在如下的示例中, Logger 的工厂构造函数从缓存中返回对象,和 Logger.fromJson 工厂构造函数从 JSON 对象中初始化一个最终变量。

class Logger {
  final String name;
  bool mute = false;

  // _cache 变量是库私有的,因为在其名字前面有下划线。
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

 备忘:

在工厂构造函数中无法访问 this

工厂构造函数的调用方式与其他构造函数一样:

var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

方法

方法是为对象提供行为的函数。

实例方法

对象的实例方法可以访问实例变量和 this。下面的 distanceTo() 方法就是一个实例方法的例子:

import 'dart:math';

class Point {
  double x, y;

  Point(this.x, this.y);

  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

操作符

运算符是有着特殊名称的实例方法。 Dart 允许您使用以下名称定义运算符:

<+|[]
>/^[]=
<=~/&~
>=*<<==
%>> 

 备忘:

你可能注意到有一些 操作符 没有出现在列表中,例如 !=。因为它们仅仅是语法糖。表达式 e1 != e2 仅仅是 !(e1 == e2) 的一个语法糖。

为了表示重写操作符,我们使用 operator 标识来进行标记。下面是重写 + 和 - 操作符的例子:

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  // Operator == and hashCode not shown.
  // ···
}

void main() {
  final v = Vector(2, 3);
  final w = Vector(2, 2);

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

Getter 和 Setter

Getter 和 Setter 是一对用来读写对象属性的特殊方法,上面说过实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,你可以使用 get 和 set 关键字为额外的属性添加 Getter 和 Setter 方法:

class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // 定义两个计算产生的属性:right 和 bottom。
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

使用 Getter 和 Setter 的好处是,你可以先使用你的实例变量,过一段时间过再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑。

 备忘:

像自增(++)这样的操作符不管是否定义了 Getter 方法都会正确地执行。为了避免一些不必要的异常情况,运算符只会调用 Getter 一次,然后将其值存储在一个临时变量中。

抽象方法

实例方法、Getter 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于 抽象类中。

直接使用分号(;)替代方法体即可声明一个抽象方法:

abstract class Doer {
  // 定义实例变量和方法等等……

  void doSomething(); // 定义一个抽象方法。
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // 提供一个实现,所以在这里该方法不再是抽象的……
  }
}

抽象类

使用关键字 abstract 标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时可被实例化,可以为其定义 工厂构造函数

抽象类常常会包含 抽象方法。下面是一个声明具有抽象方法的抽象类示例:

// This class is declared abstract and thus
// can't be instantiated.
// 该类被声明为抽象的,因此它不能被实例化。
abstract class AbstractContainer {
  // 定义构造函数、字段、方法等……

  void updateChildren(); // 抽象方法。
}

隐式接口

每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。

一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:

// A person. The implicit interface contains greet().
// Person 类的隐式接口中包含 greet() 方法。
class Person {
  // _name 变量同样包含在接口中,但它只是库内可见的。
  final _name;

  // 构造函数不在接口中。
  Person(this._name);

  // greet() 方法在接口中。
  String greet(String who) => '你好,$who。我是$_name。';
}

// Person 接口的一个实现。
class Impostor implements Person {
  get _name => '';

  String greet(String who) => '你好$who。你知道我是谁吗?';
}

String greetBob(Person person) => person.greet('小芳');

void main() {
  print(greetBob(Person('小芸')));
  print(greetBob(Impostor()));
}

如果需要实现多个类接口,可以使用逗号分割每个接口类:

class Point implements Comparable, Location {...}

扩展一个类

使用 extends 关键字来创建一个子类,并可使用 super 关键字引用一个父类:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

重写类成员

子类可以重写父类的实例方法(包括 操作符)、 Getter 以及 Setter 方法。你可以使用 @override 注解来表示你重写了一个成员:

class SmartTelevision extends Television {
  @override
  void turnOn() {...}
  // ···
}

你可以使用 covariant 关键字 来缩小代码中那些符合 类型安全 的方法参数或实例变量的类型。

 请注意:

如果重写 == 操作符,必须同时重写对象 hashCode 的 Getter 方法。你可以查阅 实现映射键 获取更多关于重写的 == 和 hashCode 的例子。

noSuchMethod 方法

如果调用了对象上不存在的方法或实例变量将会触发 noSuchMethod 方法,你可以重写 noSuchMethod 方法来追踪和记录这一行为:

class A {
  // 除非你重写 noSuchMethod,否则调用一个不存在的成员会导致 NoSuchMethodError。
  @override
  void noSuchMethod(Invocation invocation) {
  print('你尝试使用一个不存在的成员:' +
  '${invocation.memberName}');
  }
}

只有下面其中一个条件成立时,你才能调用一个未实现的方法:

  • 接收方是静态的 dynamic 类型。

  • 接收方具有静态类型,定义了未实现的方法(抽象亦可),并且接收方的动态类型实现了 noSuchMethod 方法且具体的实现与 Object 中的不同。

你可以查阅 noSuchMethod 转发规范 获取更多相关信息。

扩展方法

扩展方法是向现有库添加功能的一种方式。你可能已经在不知道它是扩展方法的情况下使用了它。例如,当您在 IDE 中使用代码完成功能时,它建议将扩展方法与常规方法一起使用。

这里是一个在 String 中使用扩展方法的样例,我们取名为 parseInt(),它在 string_apis.dart 中定义:

import 'string_apis.dart';
...
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.

有关使用以及实现扩展方法的详细信息,请参阅 扩展方法页面

枚举类型

枚举类型是一种特殊的类型,也称为 enumerations 或 enums,用于定义一些固定数量的常量值。

Using enums

使用枚举

使用关键字 enum 来定义枚举类型:

enum Color { red, green, blue }

你可以在声明枚举类型时使用 尾随逗号

每一个枚举值都有一个名为 index 成员变量的 Getter 方法,该方法将会返回以 0 为基准索引的位置值。例如,第一个枚举值的索引是 0 ,第二个枚举值的索引是 1。以此类推。

assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

想要获得全部的枚举值,使用枚举类的 values 方法获取包含它们的列表:

List<Color> colors = Color.values;
assert(colors[2] == Color.blue);

你可以在 Switch 语句中使用枚举,但是需要注意的是必须处理枚举值的每一种情况,即每一个枚举值都必须成为一个 case 子句,不然会出现警告:

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('红如玫瑰!');
    break;
  case Color.green:
    print('绿如草原!');
    break;
  default: // 没有该语句会出现警告。
    print(aColor); // 'Color.blue'
}

枚举类型有如下两个限制:

  • 枚举不能成为子类,也不可以 mix in,你也不可以实现一个枚举。

  • 不能显式地实例化一个枚举类。

你可以查阅 Dart 编程语言规范 获取更多相关信息。

使用 Mixin 为类添加功能

Mixin 是一种在多重继承中复用某个类中代码的方法模式。

使用 with 关键字并在其后跟上 Mixin 类的名字来使用 Mixin 模式:

class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

想要实现一个 Mixin,请创建一个继承自 Object 且未声明构造函数的类。除非你想让该类与普通的类一样可以被正常地使用,否则请使用关键字 mixin 替代 class。例如:

mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

可以使用关键字 on 来指定哪些类可以使用该 Mixin 类,比如有 Mixin 类 A,但是 A 只能被 B 类使用,则可以这样定义 A:

class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // ...
}

In the preceding code, only classes that extend or implement the Musician class can use the mixin MusicalPerformer. Because SingerDancer extends MusicianSingerDancer can mix in MusicalPerformer.

 版本提示:

mixin 关键字在 Dart 2.1 中才被引用支持。早期版本中的代码通常使用 abstract class 代替。你可以查阅 Dart SDK 变更日志 和 2.1 mixin 规范 获取更多有关 Mixin 在 2.1 中的变更信息。

类变量和方法

使用关键字 static 可以声明类变量或类方法。

静态变量

静态变量(即类变量)常用于声明类范围内所属的状态变量和常量:

class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}

静态变量在其首次被使用的时候才被初始化。

 备忘:

本文代码准守 风格推荐指南 中的命名规则,使用 驼峰式大小写 来命名常量。

静态方法

静态方法(即类方法)不能对实例进行操作,因此不能使用 this。但是他们可以访问静态变量。如下面的例子所示,你可以在一个类上直接调用静态方法:

import 'dart:math';

class Point {
  double x, y;
  Point(this.x, this.y);

  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

 备忘:

对于一些通用或常用的静态方法,应该将其定义为顶级函数而非静态方法。

可以将静态方法作为编译时常量。例如,你可以将静态方法作为一个参数传递给一个常量构造函数。

泛型

如果你查看数组的 API 文档,你会发现数组 List 的实际类型为 List<E>。 <…> 符号表示数组是一个 泛型(或 参数化类型) 通常 使用一个字母来代表类型参数,比如 E、T、S、K 和 V 等等。

为什么使用泛型?

泛型常用于需要要求类型安全的情况,但是它也会对代码运行有好处:

  • 适当地指定泛型可以更好地帮助代码生成。

  • 使用泛型可以减少代码重复。

比如你想声明一个只能包含 String 类型的数组,你可以将该数组声明为 List<String>(读作“字符串类型的 list”),这样的话就可以很容易避免因为在该数组放入非 String 类变量而导致的诸多问题,同时编译器以及其他阅读代码的人都可以很容易地发现并定位问题:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

另一个使用泛型的原因是可以减少重复代码。泛型可以让你在多个不同类型实现之间共享同一个接口声明,比如下面的例子中声明了一个类用于缓存对象的接口:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

不久后你可能又会想专门为 String 类对象做一个缓存,于是又有了专门为 String 做缓存的类:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

如果过段时间你又想为数字类型也创建一个类,那么就会有很多诸如此类的代码……

这时候可以考虑使用泛型来声明一个类,让不同类型的缓存实现该类做出不同的具体实现即可:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

在上述代码中,T 是一个替代类型。其相当于类型占位符,在开发者调用该接口的时候会指定具体类型。

使用集合字面量

List、Set 以及 Map 字面量也可以是参数化的。定义参数化的 List 只需在中括号前添加 <type>;定义参数化的 Map 只需要在大括号前添加 <keyTypevalueType>

var names = <String>['小芸', '小芳', '小民'];
var uniqueNames = <String>{'小芸', '小芳', '小民'};
var pages = <String, String>{
  'index.html': '主页',
  'robots.txt': '网页机器人提示',
  'humans.txt': '我们是人类,不是机器'
};

使用类型参数化的构造函数

在调用构造方法时也可以使用泛型,只需在类名后用尖括号(<...>)将一个或多个类型包裹即可:

var nameSet = Set<String>.from(names);

下面代码创建了一个键为 Int 类型,值为 View 类型的 Map 对象:

var views = Map<int, View>();

泛型集合以及它们所包含的类型

Dart的泛型类型是 固化的,这意味着即便在运行时也会保持类型信息:

var names = List<String>();
names.addAll(['小芸', '小芳', '小民']);
print(names is List<String>); // true

 备忘:

与 Java 不同的是,Java 中的泛型是类型 擦除 的,这意味着泛型类型会在运行时被移除。在 Java 中你可以判断对象是否为 List 但不可以判断对象是否为 List<String>

限制参数化类型

有时使用泛型的时候可能会想限制泛型的类型范围,这时候可以使用 extends 关键字:

class Foo<T extends SomeBaseClass> {
  // 具体实现……
  String toString() => "'Foo<$T>' 的实例";
}

class Extender extends SomeBaseClass {...}

这时候就可以使用 SomeBaseClass 或者它的子类来作为泛型参数:

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

这时候也可以指定无参数的泛型,这时无参数泛型的类型则为 Foo<SomeBaseClass>

var foo = Foo();
print(foo); // 'Foo<SomeBaseClass>' 的实例 (Instance of 'Foo<SomeBaseClass>')

将非 SomeBaseClass 的类型作为泛型参数则会导致编译错误:

var foo = Foo<Object>();

使用泛型方法

起初 Dart 只支持在类的声明时指定泛型,现在同样也可以在方法上使用泛型,称之为 泛型方法

T first<T>(List<T> ts) {
  // 处理一些初始化工作或错误检测……
  T tmp = ts[0];
  // 处理一些额外的检查……
  return tmp;
}

方法 first<T> 的泛型 T 可以在如下地方使用:

  • 函数的返回值类型 (T)。

  • 参数的类型 (List<T>)。

  • 局部变量的类型 (T tmp)。

你可以查阅 使用泛型函数 获取更多关于泛型的信息。

库和可见性

import 和 library 关键字可以帮助你创建一个模块化和可共享的代码库。代码库不仅只是提供 API 而且还起到了封装的作用:以下划线(_)开头的成员仅在代码库中可见。每个 Dart 程序都是一个库,即便没有使用关键字 library 指定。

Dart 的库可以使用 包工具 来发布和部署。

使用库

使用 import 来指定命名空间以便其它库可以访问。

比如你可以导入代码库 dart:html 来使用 Dart Web 中相关 API:

import 'dart:html';

import 的唯一参数是用于指定代码库的 URI,对于 Dart 内置的库,使用 dart:xxxxxx 的形式。而对于其它的库,你可以使用一个文件系统路径或者以 package:xxxxxx 的形式。package:xxxxxx 指定的库通过包管理器(比如 pub 工具)来提供:

import 'package:test/test.dart';

 备忘:

URI 代表统一资源标识符。

URL(统一资源定位符)是一种常见的 URI。

指定库前缀

如果你导入的两个代码库有冲突的标识符,你可以为其中一个指定前缀。比如如果 library1 和 library2 都有 Element 类,那么可以这么处理:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// 使用 lib1 的 Element 类。
Element element1 = Element();

// 使用 lib2 的 Element 类。
lib2.Element element2 = lib2.Element();

导入库的一部分

如果你只想使用代码库中的一部分,你可以有选择地导入代码库。例如:

// 只导入 lib1 中的 foo。(Import only foo).
import 'package:lib1/lib1.dart' show foo;

// 导入 lib2 中除了 foo 外的所有。
import 'package:lib2/lib2.dart' hide foo;

延迟加载库

延迟加载(也常称为 懒加载)允许应用在需要时再去加载代码库,下面是可能使用到延迟加载的场景:

  • 为了减少应用的初始化时间。

  • 处理 A/B 测试,比如测试各种算法的不同实现。

  • 加载很少会使用到的功能,比如可选的屏幕和对话框。

 

目前只有 dart2js 支持延迟加载 Flutter、Dart VM 以及 DartDevc 目前都不支持延迟加载。你可以查阅 issue #33118 和 issue #27776 获取更多的相关信息。

使用 deferred as 关键字来标识需要延时加载的代码库:

import 'package:greetings/hello.dart' deferred as hello;

当实际需要使用到库中 API 时先调用 loadLibrary 函数加载库:

Future greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

在前面的代码,使用 await 关键字暂停代码执行直到库加载完成。更多关于 async 和 await 的信息请参考异步支持

loadLibrary 函数可以调用多次也没关系,代码库只会被加载一次。

当你使用延迟加载的时候需要牢记以下几点:

  • 延迟加载的代码库中的常量需要在代码库被加载的时候才会导入,未加载时是不会导入的。

  • 导入文件的时候无法使用延迟加载库中的类型。如果你需要使用类型,则考虑把接口类型转移到另一个库中然后让两个库都分别导入这个接口库。

  • Dart会隐式地将 loadLibrary 方法导入到使用了 deferred as 命名空间 的类中。loadLibrary 函数返回的是一个 Future

实现库

查阅 创建依赖库包 可以获取有关如何实现库包的建议,包括:

  • 如何组织库的源文件。

  • 如何使用 export 命令。

  • 何时使用 part 命令。

  • 何时使用 library 命令。

  • 如何使用倒入和导出命令实现多平台的库支持。

异步支持

Dart 代码库中有大量返回 Future 或 Stream 对象的函数,这些函数都是 异步 的,它们会在耗时操作(比如I/O)执行完毕前直接返回而不会等待耗时操作执行完毕。

async 和 await 关键字用于实现异步编程,并且让你的代码看起来就像是同步的一样。

处理 Future

可以通过下面两种方式,获得 Future 执行完成的结果:

  • 使用 async 和 await

  • 使用 Future API,具体描述参考 库概览

使用 async 和 await 的代码是异步的,但是看起来有点像同步代码。例如,下面的代码使用 await 等待异步函数的执行结果。

await lookUpVersion();

必须在带有 async 关键字的 异步函数 中使用 await

Future checkVersion() async {
  var version = await lookUpVersion();
  // 使用 version 继续处理逻辑
}

 备忘:

尽管异步函数可以处理耗时操作,但是它并不会等待这些耗时操作完成,异步函数执行时会在其遇到第一个 await 表达式(代码行)时返回一个 Future 对象,然后等待 await 表达式执行完毕后继续执行。

使用 trycatch 以及 finally 来处理使用 await 导致的异常:

try {
  version = await lookUpVersion();
} catch (e) {
  // 无法找到版本时做出的反应
}

你可以在异步函数中多次使用 await 关键字。例如,下面代码中等待了三次函数结果:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await 表达式的返回值通常是一个 Future 对象;如果不是的话也会自动将其包裹在一个 Future 对象里。Future 对象代表一个“承诺”,await 表达式会阻塞直到需要的对象返回。

如果在使用 await 时导致编译错误,请确保 await 在一个异步函数中使用。例如,如果想在 main() 函数中使用 await,那么 main() 函数就必须使用 async 关键字标识。

Future main() async {
  checkVersion();
  print('在 Main 函数中执行:版本是 ${await lookUpVersion()}');
}

声明异步函数

异步函数 是函数体由 async 关键字标记的函数。

将关键字 async 添加到函数并让其返回一个 Future 对象。假设有如下返回 String 对象的方法:

String lookUpVersion() => '1.0.0';

将其改为异步函数,返回值是 Future:

Future<String> lookUpVersion() async => '1.0.0';

注意,函数体不需要使用 Future API。如有必要,Dart 会创建 Future 对象。

如果函数没有返回有效值,需要设置其返回类型为 Future<void>

关于 Future、async 和 await 的使用介绍,可以参见这个 codelab: asynchronous programming codelab

处理 Stream

如果想从 Stream 中获取值,可以有两种选择:

  • 使用 async 关键字和一个 异步循环(使用 await for 关键字标识)。

  • 使用 Stream API。详情参考 库概览

 备忘:

在使用 await for 关键字前,确保其可以令代码逻辑更加清晰并且是真的需要等待所有的结果执行完毕。例如,通常不应该在 UI 事件监听器上使用 await for 关键字,因为 UI 框架发出的事件流是无穷尽的。

使用 await for 定义异步循环看起来是这样的:

await for (varOrType identifier in expression) {
  // 每当 Stream 发出一个值时会执行
}

表达式 的类型必须是 Stream。执行流程如下:

  1. 等待直到 Stream 返回一个数据。

  2. 使用 1 中 Stream 返回的数据执行循环体。

  3. 重复 1、2 过程直到 Stream 数据返回完毕。

使用 break 和 return 语句可以停止接收 Stream 数据,这样就跳出了循环并取消注册监听 Stream。

如果在实现异步 for 循环时遇到编译时错误,请检查确保 await for 处于异步函数中。 例如,要在应用程序的 main() 函数中使用异步 for 循环,main() 函数体必须标记为 async

Future main() async {
  // ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  // ...
}

你可以查阅库概览中有关 dart:async 的部分获取更多有关异步编程的信息。

生成器

当你需要延迟地生成一连串的值时,可以考虑使用 生成器函数。Dart 内置支持两种形式的生成器方法:

  • 同步 生成器:返回一个 Iterable 对象。

  • 异步 生成器:返回一个 Stream 对象。

通过在函数上加 sync* 关键字并将返回值类型设置为 Iterable 来实现一个 同步 生成器函数,在函数中使用 yield 语句来传递值:

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

实现 异步 生成器函数与同步类似,只不过关键字为 async* 并且返回值为 Stream:

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

如果生成器是递归调用的,可是使用 yield* 语句提升执行性能:

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

可调用类

通过实现类的 call() 方法,允许使用类似函数调用的方式来使用该类的实例。

在下面的示例中,WannabeFunction 类定义了一个 call() 函数,函数接受三个字符串参数,函数体将三个字符串拼接,字符串间用空格分割,并在结尾附加了一个感叹号。单击运行按钮执行代码。

隔离区

大多数计算机中,甚至在移动平台上,都在使用多核 CPU。为了有效利用多核性能,开发者一般使用共享内存的方式让线程并发地运行。然而,多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。

为了解决多线程带来的并发问题,Dart 使用 isolate 替代线程,所有的 Dart 代码均运行在一个 isolate 中。每一个 isolate 有它自己的堆内存以确保其状态不被其它 isolate 访问。

你可以查阅下面的文档获取更多相关信息:

类型定义

在 Dart 语言中,函数与 String 和 Number 一样都是对象,可以使用 类型定义(或者叫 方法类型别名)来为函数的类型命名。使用函数命名将该函数类型的函数赋值给一个变量时,类型定义将会保留相关的类型信息。

比如下面的代码没有使用类型定义:

class SortedCollection {
  Function compare;

  SortedCollection(int f(Object a, Object b)) {
    compare = f;
  }
}

// 简单的不完整实现。
int sort(Object a, Object b) => 0;

void main() {
  SortedCollection coll = SortedCollection(sort);

  // 我们知道 compare 是一个函数类型的变量,但是具体是什么样的函数却不得而知。
  assert(coll.compare is Function);
}

上述代码中,当将参数 f 赋值给 compare 时,函数的类型信息丢失了,这里 f 这个函数的类型为 (Object, Object) → int(→ 代表返回),当然该类型也是一个 Function 的子类,但是将 f 赋值给 compare 后,f 的类型 (Object, Object) → int 就会丢失。我们可以使用 typedef 显式地保留类型信息:

typedef Compare = int Function(Object a, Object b);

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

// 简单的不完整实现。
int sort(Object a, Object b) => 0;

void main() {
  SortedCollection coll = SortedCollection(sort);
  assert(coll.compare is Function);
  assert(coll.compare is Compare);
}

 备忘:

目前类型定义只能用在函数类型上,但是将来可能会有变化。

因为类型定义只是别名,因此我们可以使用它判断任意函数类型的方法:

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

int sort(int a, int b) => a - b;

void main() {
  assert(sort is Compare<int>); // True!
}

元数据

使用元数据可以为代码增加一些额外的信息。元数据注解以 @ 开头,其后紧跟一个编译时常量(比如 deprecated)或者调用一个常量构造函数。

Dart 中有两个注解是所有代码都可以使用的:@deprecated 和 @override。你可以查阅 扩展一个类 获取有关 @override 的使用示例。下面是使用 @deprecated 的示例:

class Television {
  /// _弃用: 使用 [turnOn] 替代_
  @deprecated
  void activate() {
    turnOn();
  }

  /// 打开 TV 的电源。
  void turnOn() {...}
}

可以自定义元数据注解。下面的示例定义了一个带有两个参数的 @todo 注解:

library todo;

class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}

使用 @todo 注解的示例:

import 'todo.dart';

@Todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}

元数据可以在 library、class、typedef、type parameter、 constructor、factory、function、field、parameter 或者 variable 声明之前使用,也可以在 import 或 export 之前使用。可使用反射在运行时获取元数据信息。

注释

Dart 支持单行注释、多行注释和文档注释。

单行注释

单行注释以 // 开始。所有在 // 和该行结尾之间的内容均被编译器忽略。

void main() {
  // TODO: refactor into an AbstractLlamaGreetingFactory?
  print('Welcome to my Llama farm!');
}

多行注释

多行注释以 /* 开始,以 */ 结尾。所有在 /* 和 */ 之间的内容均被编译器忽略(不会忽略文档注释),多行注释可以嵌套。

void main() {
  /*
   * This is a lot of work. Consider raising chickens.

  Llama larry = Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}

文档注释

文档注释可以是多行注释,也可以是单行注释,文档注释以 /// 或者 /** 开始。在连续行上使用 /// 与多行文档注释具有相同的效果。

在文档注释中,除非用中括号括起来,否则 Dart 编译器会忽略所有文本。使用中括号可以引用类、方法、字段、顶级变量、函数和参数。括号中的符号会在已记录的程序元素的词法域中进行解析。

下面是一个引用其他类和成员的文档注释:

/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
class Llama {
  String name;

  /// Feeds your llama [Food].
  ///
  /// The typical llama eats one bale of hay per week.
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

在生成的文档中,[Food] 会成为一个链接,指向 Food 类的 API 文档。

解析 Dart 代码并生成 HTML 文档,可以使用 SDK 中的 文档生成工具 关于生成文档的实例,请参考 Dart API documentation 查看关于文档结构的建议,请参考文档: Guidelines for Dart Doc Comments.

总结

本页概述了 Dart 语言中常用的功能。还有更多特性有待实现,但我们希望它们不会破坏现有代码。有关更多信息,请参考 Dart 语言规范 和 高效 Dart 语言指南

要了解更多关于 Dart 核心库的内容,请参考 Dart 核心库概览

小宋是呢 CSDN认证博客专家 AI工程师 深度学习领域专家
作者简介:深度学习开发分享博主。全网粉丝3W+,阅读量200W+。
CSDN深度学习博客专家以及微信公众号《简明AI》主要作者。创作内容是基于深度学习的理论学习与应用开发技术分享,致力于最简单明了AI技术分享与最实用AI应用教程。

撰写并发表深度学习论文两篇,获得国家级及省级一等奖奖项八次,以第一作者授权实用新型及发明专利共计十余项,天池与BDCI比赛Top10奖项数次。

在某公司担任算法工程师,从事计算机视觉及时序序列数据的检测识别;深度学习工程化经验丰富,擅长针对新算法研究与应用,包括对模型调优、模型转化及多平台部署等。
相关推荐
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页
实付 69.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值