目录

Dart 开发语言概览

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

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

一个简单的 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 中一些其他的内置类型包括 StringListbool

42

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

print()

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

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

表示字符串字面量。

$variableName (或 ${expression})

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

main()

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

var

用于定义变量,通过这种方式定义变量不需要指定变量类型。这类变量的类型 (int) 由它的初始值决定 (42)。

重要概念

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

  • Everything you can place in a variable is an object, and every object is an instance of a class. Even numbers, functions, and null are objects. With the exception of null (if you enable sound null safety), all objects inherit from the Object class.

    所有变量引用的都是 对象,每个对象都是一个 的实例。数字、函数以及 null 都是对象。除去 null 以外(如果你开启了 空安全), 所有的类都继承于 Object 类。

  • 尽管 Dart 是强类型语言,但是在声明变量时指定类型是可选的,因为 Dart 可以进行类型推断。在上述代码中,变量 number 的类型被推断为 int 类型。

  • 如果你开启了 空安全,变量在未声明为可空类型时不能为 null。你可以通过在类型后加上问号 (?) 将类型声明为可空。例如,int? 类型的变量可以是整形数字或 null。如果你 明确知道 一个表达式不会为空,但 Dart 不这么认为时,你可以在表达式后添加 ! 来断言表达式不为空(为空时将抛出异常)。例如:int x = nullableButNotNullInt!

  • 如果你想要显式地声明允许任意类型,使用 Object?(如果你 开启了空安全)、 Object 或者 特殊类型 dynamic 将检查延迟到运行时进行。

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

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

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

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

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

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

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

关键字

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

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

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

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

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

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

变量

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

var name = 'Bob';

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

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

Object name = 'Bob';

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

String name = 'Bob';

默认值

在 Dart 中,未初始化以及可空类型的变量拥有一个默认的初始值 null。(如果你未迁移至 空安全,所有变量都为可空类型。)即便数字也是如此,因为在 Dart 中一切皆为对象,数字也不例外。

int? lineCount;
assert(lineCount == null);

If you enable null safety, then you must initialize the values of non-nullable variables before you use them:

int lineCount = 0;

You don’t have to initialize a local variable where it’s declared, but you do need to assign it a value before it’s used. For example, the following code is valid because Dart can detect that lineCount is non-null by the time it’s passed to print():

int lineCount;

if (weLikeToCount) {
  lineCount = countLines();
} else {
  lineCount = 0;
}

print(lineCount);

Top-level and class variables are lazily initialized; the initialization code runs the first time the variable is used.

Late variables

Dart 2.12 added the late modifier, which has two use cases:

  • Declaring a non-nullable variable that’s initialized after its declaration.
  • Lazily initializing a variable.

Often Dart’s control flow analysis can detect when a non-nullable variable is set to a non-null value before it’s used, but sometimes analysis fails. Two common cases are top-level variables and instance variables: Dart often can’t determine whether they’re set, so it doesn’t try.

If you’re sure that a variable is set before it’s used, but Dart disagrees, you can fix the error by marking the variable as late:

late String description;

void main() {
  description = 'Feijoada!';
  print(description);
}

When you mark a variable as late but initialize it at its declaration, then the initializer runs the first time the variable is used. This lazy initialization is handy in a couple of cases:

  • The variable might not be needed, and initializing it is costly.
  • You’re initializing an instance variable, and its initializer needs access to this.

In the following example, if the temperature variable is never used, then the expensive readThermometer() function is never called:

// This is the program's only call to readThermometer().
late String temperature = readThermometer(); // Lazily initialized.

Final 和 Const

如果你不想更改一个变量,可以使用关键字 final 或者 const 修饰变量,这两个关键字可以替代 var 关键字或者加在一个具体的类型前。一个 final 变量只可以被赋值一次;一个 const 变量是一个编译时常量(const 变量同时也是 final 的)。顶层的 final 变量或者类的 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; // Standard atmosphere

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

var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`

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

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

foo = [1, 2, 3]; // Was const []

常量的值不可以被修改:

baz = [42]; // Error: Constant variables can't be assigned a value.

你可以在常量中使用 类型检查和强制类型转换 (isas)、 集合中的 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.

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

内置类型

Dart 语言支持下列内容:

使用字面量来创建对象也受到支持。例如 'This is a string' 是一个字符串字面量,true 是一个布尔字面量。

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

Some other types also have special roles in the Dart language:

  • Object: The superclass of all Dart classes except Null.
  • Enum: The superclass of all enums.
  • Future and Stream: Used in asynchrony support.
  • Iterable: Used in for-in loops and in synchronous generator functions.
  • Never: Indicates that an expression can never successfully finish evaluating. Most often used for functions that always throw an exception.
  • dynamic: Indicates that you want to disable static checking. Usually you should use Object or Object? instead.
  • void: Indicates that a value is never used. Often used as a return type.

The Object, Object?, Null, and Never classes have special roles in the class hierarchy, as described in the top-and-bottom section of Understanding null safety.

Numbers

Dart 支持两种 Number 类型:

int

整数值;长度不超过 64 位,具体取值范围 依赖于不同的平台。在 DartVM 上其取值位于 -263 至 263 - 1 之间。在 Web 上,整型数值代表着 JavaScript 的数字(64 位无小数浮点型),其允许的取值范围在 -253 至 253 - 1 之间。

double

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

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

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

var x = 1;
var hex = 0xDEADBEEF;

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

var y = 1.1;
var exponents = 1.42e5;

You can also declare a variable as a num. If you do this, the variable can have both integer and double values.

num x = 1; // x can have both int and double values
x += 2.5;

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

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

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

// 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 | 4) == 7); // 0011 | 0100 == 0111
assert((3 & 4) == 0); // 0011 & 0100 == 0000

更多示例请查看 移位操作符 小节。

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

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

更多内容,请查看 Dart 中的数字

Strings

Dart 字符串(String 对象)包含了 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 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";
// 代码中文解释
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 字符的信息。

字符串字面量是一个编译时常量,只要是编译时常量 (null、数字、字符串、布尔) 都可以作为字符串字面量的插值表达式:

// These work in a const string.
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// These do NOT work in a const 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';

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

布尔类型

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

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

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);

// Check for null.
var unicorn;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

Lists

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

Dart 中的列表字面量是由逗号分隔的一串表达式或值并以方括号 ([]) 包裹而组成的。下面是一个 Dart List 的示例:

var list = [1, 2, 3];

你可以在 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 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');

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

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

Sets

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

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

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

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

var names = <String>{};
// Set<String> names = {}; // This works, too.
// var names = {}; // Creates a map, not a set.

使用 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 iffor 操作。你可以查阅 List 扩展操作符List 集合操作符 获取更多相关信息。

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

Maps

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

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

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

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

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

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

var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

向现有的 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 API 获取更多相关信息。

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 run bin/main.dart
Hi 🇩🇰
The end of the string: ???
The last character: 🇩🇰

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

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 表达式; } 的简写, => 有时也称之为 箭头 函数。

参数

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

向函数传入参数或者定义函数参数时,可以使用 尾逗号

命名参数

命名参数默认为可选参数,除非他们被特别标记为 required

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

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool? bold, bool? hidden}) {...}

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

enableFlags(bold: true, hidden: false);

尽管先使用位置参数会比较合理,但你也可以在任意位置使用命名参数,让整个调用的方式看起来更适合你的 API:

repeat(times: 2, () {
  ...
});

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

const Scrollbar({super.key, required Widget child});

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

可选的位置参数

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

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

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

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold will be true; hidden will be false.
enableFlags(bold: true);

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

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() 的函数,并为其名为 listgifts 的参数指定了一个 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> 类型的可选参数。

下面是一个简单 main() 函数:

void main() {
  print('Hello, World!');
}

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

// 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];

// Pass printElement as a parameter.
list.forEach(printElement);

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

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

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

匿名函数

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

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

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

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

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

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

点击 Run 按钮执行代码。

void main() {
  const 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 变量。

/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // Create a function that adds 2.
  var add2 = makeAdder(2);

  // Create a function that adds 4.
  var add4 = makeAdder(4);

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

测试函数是否相等

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

void foo() {} // A top-level function

class A {
  static void bar() {} // A static method
  void baz() {} // An instance method
}

void main() {
  Function x;

  // Comparing top-level functions.
  x = foo;
  assert(foo == x);

  // Comparing static methods.
  x = A.bar;
  assert(A.bar == x);

  // Comparing instance methods.
  var v = A(); // Instance #1 of A
  var w = A(); // Instance #2 of A
  var y = w;
  x = w.baz;

  // These closures refer to the same instance (#2),
  // so they're equal.
  assert(y.baz == x);

  // These closures refer to different instances,
  // so they're unequal.
  assert(v.baz != w.baz);
}

返回值

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

foo() {}

assert(foo() == null);

运算符

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

描述 运算符
一元后缀 表达式++ 表达式-- () [] . ?. !
一元前缀 -表达式 !表达式 ~表达式 ++表达式 --表达式
乘除法 * / % ~/
加减法 + -
位运算 << >> >>>
二进制与 &
二进制异或 ^
二进制或 |
关系和类型测试 >= > <= < as is is!
相等判断 == !=
逻辑与 &&
逻辑或 ||
空判断 ??
条件表达式 表达式 1 ? 表达式 2 : 表达式 3
级联 .. ?..
赋值 = *= /= += -= &= ^= 等等……

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

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

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

// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) ...

// Harder to read, but equivalent.
if (n % i == 0 && d % i == 0) ...

算术运算符

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

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

示例:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder

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)
--var var = var - 1 (表达式的值为 var - 1)
var-- var = var - 1 (表达式的值为 var)

示例:

int a;
int b;

a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1

a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0

a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1

a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b); // -1 != 0

关系运算符

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

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

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

  1. xy 同时为空时返回 true,而只有一个为空时返回 false。

  2. 返回对 x 调用 == 方法的结果,参数为 y。(像 == 这样的操作符是对左侧内容进行调用的。详情请查阅 操作符。)

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

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

类型判断运算符

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

Operator Meaning
as 类型转换(也用作指定 类前缀))
is 如果对象是指定类型则返回 true
is! 如果对象是指定类型则返回 false

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

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

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

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

if (employee is Person) {
  // Type check
  employee.firstName = 'Bob';
}

赋值运算符

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

// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;

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

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

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

场景 复合运算 等效表达式
假设有运算符 op a op= b a = a op b
示例: a += b a = 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
assert((value >>> 4) == 0x02); // Unsigned shift right
assert((-value >> 4) == -0x03); // Shift right
assert((-value >>> 4) > 0); // Unsigned shift right

条件表达式

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

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

表达式 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';

// Very long version uses if-else statement.
String playerName(String? name) {
  if (name != null) {
    return name;
  } else {
    return 'Guest';
  }
}

级联运算符

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

比如下面的代码:

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

The constructor, Paint(), returns a Paint object. The code that follows the cascade notation operates on this object, ignoring any values that might be returned.

The previous example is equivalent to this code:

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

If the object that the cascade operates on can be null, then use a null-shorting cascade (?..) for the first operation. Starting with ?.. guarantees that none of the cascade operations are attempted on that null object.

querySelector('#confirm') // Get an object.
  ?..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'))
  ..scrollIntoView();

上面的代码相当于:

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

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

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'); // Error: method 'write' isn't defined for 'void'.

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

其他运算符

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

运算符 名字 描述
() 使用方法 代表调用一个方法
[] 访问 List 访问 List 中特定位置的元素
?[] 判空访问 List 左侧调用者不为空时,访问 List 中特定位置的元素
. 访问成员 成员访问符
?. 条件访问成员 与上述成员访问符类似,但是左边的操作对象不能为 null,例如 foo?.bar,如果 foo 为 null 则返回 null ,否则返回 bar
! 空断言操作符 将表达式的类型转换为其基础类型,如果转换失败会抛出运行时异常。例如 foo!.bar,如果 foo 为 null,则抛出运行时异常

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

流程控制语句

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

  • ifelse

  • for 循环

  • whiledo-while 循环

  • breakcontinue

  • switchcase

  • assert

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

If 和 Else

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

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

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());

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

如果要遍历的对象是一个可迭代对象(例如 List 或 Set),并且你不需要知道当前的遍历索引,则可以使用 for-in 方法进行 遍历

for (final candidate in candidates) {
  candidate.interview();
}

可迭代对象同时可以使用 forEach() 方法作为另一种选择:

var collection = [1, 2, 3];
collection.forEach(print); // 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 语句中使用。

每一个非空的 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();
    // ERROR: Missing break

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

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

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // Empty case falls through.
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

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

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // Continues executing at the nowClosed label.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

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

断言

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

// Make sure the variable has a non-null value.
assert(text != null);

// Make sure the value is less than 100.
assert(number < 100);

// Make sure this is an https URL.
assert(urlString.startsWith('https'));

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

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

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

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

  • Flutter 在 调试模式 时生效。

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

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

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

异常

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

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

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

抛出异常

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

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

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

throw 'Out of llamas!';

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

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

捕获异常

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

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

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

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

如上述代码所示可以使用 oncatch 来捕获异常,使用 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++); // Runtime error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

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

Finally

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

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

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

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

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

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

使用类的成员

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

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

var p = Point(2, 2);

// Get the value of y.
assert(p.y == 2);

// Invoke distanceTo() on p.
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});

一些类提供了常量构造函数。使用常量构造函数,在构造函数名之前加 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 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 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!

获取对象的类型

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

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

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

实例变量

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

class Point {
  double? x; // Declare instance variable x, initially null.
  double? y; // Declare y, initially null.
  double z = 0; // Declare z, initially 0.
}

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

所有实例变量均会隐式地声明一个 Getter 方法。非终值的实例变量和 late final 声明但未声明初始化的实例变量还会隐式地声明一个 Setter 方法。你可以查阅 Getter 和 Setter 获取更多相关信息。

class Point {
  double? x; // Declare instance variable x, initially null.
  double? y; // Declare y, initially null.
}

void main() {
  var point = Point();
  point.x = 4; // Use the setter method for x.
  assert(point.x == 4); // Use the getter method for x.
  assert(point.y == null); // Values default to null.
}

Instance variables can be final, in which case they must be set exactly once. Initialize final, non-late instance variables at declaration, using a constructor parameter, or using a constructor’s initializer list:

class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

If you need to assign the value of a final instance variable after the constructor body starts, you can use one of the following:

构造函数

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

class Point {
  double x = 0;
  double y = 0;

  Point(double x, double y) {
    // See initializing formal parameters for a better way
    // to initialize instance variables.
    this.x = x;
    this.y = y;
  }
}

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

终值初始化

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

构造中初始化的参数可以用于初始化非空或 final 修饰的变量,它们都必须被初始化或提供一个默认值。

class Point {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  Point(this.x, this.y);
}

在初始化时出现的变量默认是隐式终值,且只在初始化时可用。

默认构造函数

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

构造函数不被继承

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

命名式构造函数

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

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

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

调用父类非默认构造函数

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

  1. 初始化列表

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

  3. 当前类的构造函数

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

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

class Person {
  String? firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson().
  Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
  }
}

void main() {
  var employee = Employee.fromJson({});
  print(employee);
  // Prints:
  // in Person
  // in Employee
  // Instance of 'Employee'
}

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

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

超类参数

为了不重复地将参数传递到超类构造的指定参数,你可以使用超类参数,直接在子类的构造中使用超类构造的某个参数。超类参数不能和重定向的参数一起使用。超类参数的表达式和写法与 终值初始化 类似:

class Vector2d {
  final double x;
  final double y;

  Vector2d(this.x, this.y);
}

class Vector3d extends Vector2d {
  final double z;

  // Forward the x and y parameters to the default super constructor like:
  // Vector3d(final double x, final double y, this.z) : super(x, y);
  Vector3d(super.x, super.y, this.z);
}

如果超类构造的位置参数已被使用,那么超类构造参数就不能再继续使用被占用的位置。但是超类构造参数可以始终是命名参数:

class Vector2d {
  // ...

  Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
  // ...

  // Forward the y parameter to the named super constructor like:
  // Vector3d.yzPlane({required double y, required this.z})
  //       : super.named(x: 0, y: y);
  Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}

初始化列表

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

// 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)');
}

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

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

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

import 'dart:math';

class Point {
  final double x;
  final double y;
  final double distanceFromOrigin;

  Point(double x, double y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

void main() {
  var p = Point(2, 3);
  print(p.distanceFromOrigin);
}

重定向构造函数

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

class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  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 is library-private, thanks to
  // the _ in front of its name.
  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);
  }
}

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

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

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

方法

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

实例方法

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

import 'dart:math';

class Point {
  final double x;
  final double 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 允许您使用以下名称定义运算符:

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

为了表示重写操作符,我们使用 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 方法,你可以使用 getset 关键字为额外的属性添加 Getter 和 Setter 方法:

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

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

  // Define two calculated properties: right and 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 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于 抽象类中。

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

abstract class Doer {
  // Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}

抽象类

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

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

// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {
  // Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}

隐式接口

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

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

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final String _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  String get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

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

void main() {
  print(greetBob(Person('Kathy')));
  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();
  }
  // ···
}

想了解其他 extends 的用法,请查看 泛型 部分中的 参数化类型

重写类成员

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

class Television {
  // ···
  set contrast(int value) {...}
}

class SmartTelevision extends Television {
  @override
  set contrast(num value) {...}
  // ···
}

An overriding method declaration must match the method (or methods) that it overrides in several ways:

  • The return type must be the same type as (or a subtype of) the overridden method’s return type.
  • Argument types must be the same type as (or a supertype of) the overridden method’s argument types. In the preceding example, the contrast setter of SmartTelevision changes the argument type from int to a supertype, num.
  • If the overridden method accepts n positional parameters, then the overriding method must also accept n positional parameters.
  • A generic method can’t override a non-generic one, and a non-generic method can’t override a generic one.

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

noSuchMethod 方法

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

class A {
  // Unless you override noSuchMethod, using a
  // non-existent member results in a NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: '
        '${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.

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

枚举类型

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

声明简单的枚举

你可以使用关键字 enum 来定义简单的枚举类型和枚举值:

enum Color { red, green, blue }

声明增强的枚举类型

Dart 中的枚举也支持定义字段、方法和常量构造,常量构造只能构造出已知数量的常量实例(已定义的枚举值)。

你可以使用与定义 类似的语句来定义增强的枚举,但是这样的定义有一些限制条件:

  • 实例的字段必须是 final,包括由 mixin 混入的字段。

  • 所有的 实例化构造 必须以 const 修饰。

  • 工厂构造 只能返回已知的一个枚举实例。

  • 由于 Enum 已经自动进行了继承,所以枚举类不能再继承其他类。

  • 不能重载 indexhashCode 和比较操作符 ==

  • 不能声明 values 字段,否则它将与枚举本身的静态 values getter 冲突。

  • 在进行枚举定义时,所有的实例都需要首先进行声明,且至少要声明一个枚举实例。

下方是一个增强枚举的例子,它包含多个枚举实例、成员变量、getter 并且实现了接口:

enum Vehicle implements Comparable<Vehicle> {
  car(tires: 4, passengers: 5, carbonPerKilometer: 400),
  bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
  bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);

  const Vehicle({
    required this.tires,
    required this.passengers,
    required this.carbonPerKilometer,
  });

  final int tires;
  final int passengers;
  final int carbonPerKilometer;

  int get carbonFootprint => (carbonPerKilometer / passengers).round();

  @override
  int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}

想要了解更多关于定义增强枚举的内容,可以阅读 小节。

使用枚举

你可以像访问 静态变量 一样访问枚举值:

final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
  print('Your favorite color is 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('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
}

如果你想要获取一个枚举值的名称,例如 Color.blue'blue',请使用 .name 属性:

print(Color.blue.name); // 'blue'

使用 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 Musician, SingerDancer can mix in MusicalPerformer.

类变量和方法

使用关键字 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 = <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 只需要在大括号前添加 <keyType, valueType>

var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

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

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

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

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

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

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

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

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

限制参数化类型

有时使用泛型的时候,你可能会想限制可作为参数的泛型范围,也就是参数必须是指定类型的子类,这时候可以使用 extends 关键字。

一种常见的非空类型处理方式,是将子类限制继承 Object (而不是默认的 Object?)。

class Foo<T extends Object> {
  // Any type provided to Foo for T must be non-nullable.
}

You can use extends with other types besides Object. Here’s an example of extending SomeBaseClass, so that members of SomeBaseClass can be called on objects of type T:

class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

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

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

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

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

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

var foo = Foo<Object>();

使用泛型方法

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

T first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

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

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

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

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

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

库和可见性

importlibrary 关键字可以帮助你创建一个模块化和可共享的代码库。代码库不仅只是提供 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';

指定库前缀

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

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

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

导入库的一部分

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

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

延迟加载库

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

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

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

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

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

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

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

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

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

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

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

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

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

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

实现库

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

  • 如何组织库的源文件。

  • 如何使用 export 命令。

  • 何时使用 part 命令。

  • 何时使用 library 命令。

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

异步支持

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

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

处理 Future

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

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

await lookUpVersion();

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

Future<void> checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

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

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

你可以在异步函数中多次使用 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 关键字标识。

void main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

For an interactive introduction to using futures, async, and await, see the asynchronous programming codelab.

声明异步函数

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

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

String lookUpVersion() => '1.0.0';

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

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

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

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

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

处理 Stream

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

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

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

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

await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

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

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

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

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

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

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

void main() async {
  // ...
  await for (final 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() 函数,函数接受三个字符串参数,函数体将三个字符串拼接,字符串间用空格分割,并在结尾附加了一个感叹号。单击运行按钮执行代码。

class WannabeFunction {
  String call(String a, String b, String c) => '$a $b $c!';
}

var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');

void main() => print(out);

隔离区

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

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

所有的 Dart 代码都是在一个 isolate 中运行,而非线程。每个 isolate 都有一个单独的执行线程,并且不与其他的 isolate 共享任何可变对象。

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

Typedefs

类型别名是引用某一类型的简便方法,因为其使用关键字 typedef,因此通常被称作 typedef。下面是一个使用 IntList 来声明和使用类型别名的例子:

typedef IntList = List<int>;
IntList il = [1, 2, 3];

类型别名可以有类型参数:

typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

针对函数,在大多数情况下,我们推荐使用 内联函数类型 替代 typedefs。然而,函数的 typedefs 仍然是有用的:

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@Deprecated@override。你可以查阅 扩展一个类 获取有关 @override 的使用示例。下面是使用 @deprecated 的示例:

class Television {
  /// Use [turnOn] to turn the power on instead.
  @Deprecated('Use turnOn instead')
  void activate() {
    turnOn();
  }

  /// Turns the TV's power on.
  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();
   */
}

文档注释

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

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

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

/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
///
/// Just like any other animal, llamas need to eat,
/// so don't forget to [feed] them some [Food].
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) {
    // ...
  }
}

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

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

总结

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

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