Dart编程语言导览
本文展示如何使用各个主要Dart功能,从变量、运算符到类和库,这里假定你已经至少了解如何使用一种其它编程语言。
要了解更多有关 Dart核心库的知识,请参见核心库导览。在想要了解更多有关语言功能的时候,请查询Dart语言规范。
一个Dart基础程序
以下代码使用了Dart的大量最基础的功能:
1 2 3 4 5 6 7 8 9 10 |
// 定义一个函数 printInteger(int aNumber) { print('The number is $aNumber.'); // 打印到控制台 } // 这是应用开始执行的地方。 main() { var number = 42; // 声明和初始化一个变量 printInteger(number); // 调用函数 } |
以下是这个程序所使用到的可用于所有(或几乎所有)的Dart应用的部分:
// 这是一条注释
- 单行注释。Dart还支持多行注释和文件注释。更多详情,请参见 注释.
int
- 一种类型。其它的一些 内置类型 有
String
,List
和bool
。 42
- 一个数字字面量。数字字面量是一种编译时常量。
print()
- 一种显示输出的方便的方式。
'...'
(or"..."
)- 一个字符串字面量。
$variableName
(或${expression}
)- 插值字符串:包含等价于字符串字面量的变量或表达式的字符串。更多相关信息,请参见 字符串.
main()
- 在应用执行开始处的特殊的、必须的顶级函数。更多相关信息,请参见 main()函数.
var
- 一种无需指定类型声明变量的方式。
Note: 本站的代码遵循 Dart 样式指南中的准则。
重要概念
在学习Dart编程语言的过程中,请在心中保持以下事实和概念:
- 在变量中放置的所有内容都是对象,并且每个对象都是一个类的实例。包括数字、函数和
null
都是对象。所有的对象都继承自 Object 类。 - 虽然Dart是强类型语言,类型标注却是可选的,因为Dart可以推导类型。在以上的代码中,
number
所推导的类型是int
。在你想要显式地说明无需设定类型, 使用特殊类型dynamic
. - Dart支持泛型,如
List<int>
(整型列表) 或List<dynamic>
(任意类型对象的列表)。 - Dart支持顶级函数(如
main()
),以及与类或对象绑定的函数 (分别为静态和实例方法)。你也可以在函数内创建函数(嵌套或局部函数)。 - 类型地,Dart支持顶级变量,以及与类或对象绑定的变量(静态和实例变量)。实例变量有时也称为字段或属性。
- 不同于Java,Dart没有
public
,protected
和private
这些关键字。 如果一个标识符以下划线(_)开头,它是对库私有的。详情请参见 库和可见性. - 标识符可以字母或下划线 (_)开头,后接这些字符和数字的任意组合。
- Dart既有表达式(拥有运行时值)也有语句(没有运行时值)。例如, 条件表达式
condition ? expr1 : expr2
有一个值expr1
或expr2
。对比 if-else 语句则没有值。一条语句通常包含一个或多个表达式,但一个表达式不能直接包含一条语句。 - Dart工具可以报出两类问题:警告(warning)和错误(error) 。警告只是表明你的代码可能无法运行,但并不会阻止程序的执行。错误可以是编译时或运行时的。编译时错误会完全阻止代码的执行,运行时错误会在代码执行时导致 异常 的抛出。
关键字
下表中列出了Dart编程语言中具有特殊含义的单词。
abstract 2 | dynamic 2 | implements 2 | show 1 |
as 2 | else | import 2 | static 2 |
assert | enum | in | super |
async 1 | export 2 | interface 2 | switch |
await 3 | extends | is | sync 1 |
break | external 2 | library 2 | this |
case | factory 2 | mixin 2 | throw |
catch | false | new | true |
class | final | null | try |
const | finally | on 1 | typedef 2 |
continue | for | operator 2 | var |
covariant 2 | Function 2 | part 2 | void |
default | get 2 | rethrow | while |
deferred 2 | hide 1 | return | with |
do | if | set 2 | yield 3 |
避免使用这些词作为标识符。但是在必要时标记有上标文本的关键字可作为标识符:
- 上标为 1 的词为上下文关键字,仅在指定位置具有含义。它们在任何地方都是有效的标识符。
- 上标为 2 的词是内置标识符。为简化将JavaScript代码移植到Dart的任务,这些关键字在大部分地方是有效的关键字,但不能用于类或类型名称,或者是作为导入的前缀。
- 上标为 3 的词是在Dart 1.0版本之后添加的与异步支持相关的更新的、有限的保留词。你无法使用
async
,async*
或sync*
标记的函数体内使用await
或yield
作为标识符。
表中的其它词都是保留词,无法用作标识符。
变量
以下是一个创建变量并初始化的示例:
1 |
var name = 'Bob'; |
变量存储引用。名为name
的变量包含一个对值为”Bob”的String
对象的引用。
变量name
的类型推导为String
,但你可以通过指定类型来进行修改。如果对象不仅限于单个类型,按照设计指南将其指定为Object
或dynamic
类型。
1 |
dynamic name = 'Bob'; |
另一个选项是显式地将其声明为所要推导的类型:
1 |
String name = 'Bob'; |
Note: 本页中对局部变量按照 样式指南推荐 使用 var
,而非类型注解。
默认值
未初始化过的值有一个初始值null
。即使是带有数值类型的变量初始值也是null,因为数值类型和Dart中的所有内容一样,都是对象。
1 2 |
int lineCount; assert(lineCount == null); |
Note: 生产代码会忽略 assert()
调用。而在开发期间,当 if 条件为假时 assert(condition)
会抛出一个异常。详情请参见 Assert。
Final和const
如果你不想要修改一个变量,使用 final
或 const
来替代 var
或加前类型前。final变量只能设置一次,const变量是编译时常量。(const变量是隐式的final)。final顶级或类变量在初次使用时进行初始化。
Note: 实例变量可以是 final
,但不能为 const
。final实例变量必须在构造函数体开始之前进行初始化,通过构造函数参数在变量声明中或在构造函数的初始化程序列表中。
以下是一个创建和设置final变量的示例:
1 2 |
final name = 'Bob'; // 无类型标注 final String nickname = 'Bobby'; |
你无法修改final变量的值:
1 |
name = 'Alice'; // Error: a final variable can only be set once. |
对你希望是编译时常量的变量使用const
。如果const变量是类级别的,将其标记为 static const
。在声明变量之处,设置值为编译时常量,如数值或字符串字面量、const变量或对常数的算术运算结果:
1 2 |
const bar = 1000000; // 单位压强 (dynes/cm2) const double atm = 1.01325 * bar; // 标准大气 |
const
关键字不只是为声明常变量的。你还可以使用它来创建常量值,以及声明创建常量值的构造函数。任意变量可拥有常量值。
1 2 3 |
var foo = const []; final bar = const []; const baz = []; // 等价于 const [] |
你可以在const
声明的初始化表达式中省略 const
,像上面的 baz
。详情请参见 不要重复使用const.
你可以修改一个非final、非const变量的值,即使它曾经是一个const值:
1 |
foo = [1, 2, 3]; // 曾经是const [] |
但是不能修改const变量的值:
1 |
baz = [42]; // Error: Constant variables can't be assigned a value. |
更多有关使用 const
创建常量值的内容,请参见 列表, 映射 和 类。
内置类型
Dart语言拥有对如下类型的特别支持:
- 数值
- 字符串
- 布尔型
- 列表 (也称为数组)
- 集
- 映射
- rune (用于在字符串表示Unicode字符串)
- 符号
你可以使用字面量初始化任意这些特殊类型的对象。例如, 'this is a string'
是一个字符串字面量, true
是一个布尔型字面量。
因为Dart中的每个变量都引用一个对象 – 一个类的实例,通常可以使用构造函数来初始化变量。一些内置类型有它们自己的构造函数。例如,你可以使用 Map()
构造函数来创建一个映射。
数值
Dart有两种数值类型:
- int
- 整型值根据平台不大于64位。在 Dart VM中,值可以为 -263 到 263 – 1。编译为JavaScript的Dart使用 JavaScript数值, 允许的值为 -253 到 253 – 1。
- double
- 64位(双精度)浮点数值,如IEEE 754 标准中所描述。
int
和 double
是 num
的子类型。num类型包含基本运算符如 +, -, / 和 *,也包含其它方法中的 abs()
,ceil()
和 floor()
。(位相关的运算符,如 >>,在 int
类中定义)。 如果num及其子类型中没有你所要寻找的,可以使用 dart:math 库。
整型是不包含小数点的数字。以下是一些定义整型字面量的示例:
1 2 |
var x = 1; var hex = 0xDEADBEEF; |
如果数值中包含小数点,则为double类型。以下是一些定义double字面量的示例:
1 2 |
var y = 1.1; var exponents = 1.42e5; |
在 Dart 2.1中,整型字面在需要时会自动转化为double值:
1 |
double z = 1; // 等价于 z = 1.0. |
以下是如何将字符串转化为数字及其反向操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 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'); |
int类型指定了传统的按钮移动 (<<, >>)、与 (&)和 或 (|)运算符。例如:
1 2 3 |
assert((3 << 1) == 6); // 0011 << 1 == 0110 assert((3 >> 1) == 1); // 0011 >> 1 == 0001 assert((3 | 4) == 7); // 0011 | 0100 == 0111 |
字面量数字是编译时常量。很多算术表达式也是编译时常量,只要它们的运算项是运算为数值的编译时常量。
1 2 3 |
const msPerSecond = 1000; const secondsUntilRetry = 5; const msUntilRetry = secondsUntilRetry * msPerSecond; |
字符串
Dart字符串是一个UTF-16代码单元。你可以使用单引号或双引号来创建字符串:
1 2 3 4 |
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."; |
可以通过使用${
expression
}
来将表达式的值放到字符串中。如果表达式是一个标识符,可以省略{}。要获取一个对象对应的字符串,Dart中调用对象的toString()
方法。
1 2 3 4 5 6 7 8 9 |
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!'); |
Note: ==
运算符用于测试两个对象是否相等。如果两个字符串拥有相同序列的代码单元则相等。
可以使用相邻字符串字面量或者了 +
运算符来拼接字符串:
1 2 3 4 5 6 7 8 9 |
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.'); |
另一种创建多行字符串的方式是使用三个单引号或三个双引号标记:
1 2 3 4 5 6 7 |
var s1 = ''' You can create multi-line strings like this one. '''; var s2 = """This is also a multi-line string."""; |
可以通过r
前缀来创建“原生”字符串:
1 |
var s = r'In a raw string, not even \n gets special treatment.'; |
参见 Rune 来获取如何在字符串中表达Unicode字符的详情。
字符串字面量是编译时常量,仅需编译时常量中的插值表达式运行结果为null或数值、字符串或布尔值。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 这些可以在一个const字符串中使用 const aConstNum = 0; const aConstBool = true; const aConstString = 'a constant string'; // 这些不可以在一个const字符串中使用 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
的类型。仅有两个对象的类型为bool:布尔型字面量true
和 false
,它们都是编译时常量。
Dart的类型安全意味着你不能使用if (nonbooleanValue)
或 assert (nonbooleanValue)
这样的代码。而是要显式的检查值,类似这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 检查空字符串 var fullName = ''; assert(fullName.isEmpty); // 检查0值 var hitPoints = 0; assert(hitPoints <= 0); // 检查null值 var unicorn; assert(unicorn == null); // 检查NaN var iMeantToDoThis = 0 / 0; assert(iMeantToDoThis.isNaN); |
列表
或许在几乎所有编程语言中最常见的集合(collection)都是数组,或有序的对象组。在Dart,数组是List对象,因此大部分会称其为列表。
Dart的列表字面量和JavaScript中的数组字面量很像。以下是一个简单的Dart列表:
1 |
var list = [1, 2, 3]; |
列表使用基于0的索引,即0是第1个元素的索引,并且 list.length - 1
是最后一个元素的索引。你可以完全像JavaScript中那样获取一个列表的长度并引用列表元素:
1 2 3 4 5 6 |
var list = [1, 2, 3]; assert(list.length == 3); assert(list[1] == 2); list[1] = 1; assert(list[1] == 1); |
要创建一个为运行是常量的列表,在列表字面量之前添加 const
:
1 2 |
var constantList = const [1, 2, 3]; // constantList[1] = 1; // 取消这行的注释会导致报错 |
Dart 2.3 引入了展开运算符(...
) 及判空展开运算符 (...?
),提供一种向集合插入多个元素的简洁方式。
例如,你可以使用展开运算符 (...
) 来将列表中的所有元素插入到另一个列表中:
1 2 3 |
var list = [1, 2, 3]; var list2 = [0, ...list]; assert(list2.length == 4); |
如果展开运算符右侧的表达式有可能为null,可通过使用可判空展开运算符(...?
)来避免异常:
1 2 3 |
var list; var list2 = [0, ...?list]; assert(list2.length == 1); |
更多有关使用展开运算符的详情和示例,参见展开运算符提议。
Dart 2.3 还引入了collection if 和 collection for,可以使用条件(if
)和循环 (for
)来构建使用条件(if
)和循环 (for
)的集合。
以下是使用collection if来创建一个包含3项或4项的列表::
1 2 3 4 5 6 |
var nav = [ 'Home', 'Furniture', 'Plants', if (promoActive) 'Outlet' ]; |
以下是使用collection for来在将它们添加到另一个列表之前操作列表项的示例:
1 2 3 4 5 6 |
var listOfInts = [1, 2, 3]; var listOfStrings = [ '#0', for (var i in listOfInts) '#$i' ]; assert(listOfStrings[1] == '#1'); |
更多有关使用collection if和for的示例,参见 控制流collection建议。
列表类型有很多操作列表的便捷的方法。有关列表更多的信息,请参见泛型 和 集合.
集
Dart中的集(set)是一个包含独立项的无序集合。Dart对集的支持由集字面量和Set类型所提供。
以下是一个简单的Dart集,使用集字面量创建:
1 |
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'}; |
要创建空集,使用带有类型前缀的{}
或向类型为Set
的变量赋值{}
:
1 2 3 |
var names = <String>{}; // Set<String> names = {}; // 这也同样起作用 // var names = {}; // 创建一个映射,而非集 |
使用add()
或addAll()
方法向已有集添加子项:
1 2 3 |
var elements = <String>{}; elements.add('fluorine'); elements.addAll(halogens); |
使用 .length
来获取集中子项的数量:
1 2 3 4 |
var elements = <String>{}; elements.add('fluorine'); elements.addAll(halogens); assert(elements.length == 5); |
创建为运行时常量的集,在集字面量前添加const
:
1 2 3 4 5 6 7 8 |
final constantSet = const { 'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine', }; // constantSet.add('helium'); // 取消本行注释会导致报错 |
在Dart 2.3中,集也像列表一样支持展开运算符 (...
和 ...?
) 以及 collection if 和 for。更多信息,参见 列表展开运算符 和 列表集合运算符 部分的讨论。
映射
总的来说,映射是一个关联键和值的对象。键和值都可以为任意对象类型。每个键仅出现一次,但相同值可出现多次。Dart对映射的支持由映射字面量和 Map 类型所提供。
以下是一些简单的Dart映射,使用映射字面量创建:
1 2 3 4 5 6 7 8 9 10 11 12 |
var gifts = { // Key: Value 'first': 'partridge', 'second': 'turtledoves', 'fifth': 'golden rings' }; var nobleGases = { 2: 'helium', 10: 'neon', 18: 'argon', }; |
可以使用Map构造函数创建相同的对象:
1 2 3 4 5 6 7 8 9 |
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'; |
和JavaScript中一样向已有映射添加键值对:
1 2 |
var gifts = {'first': 'partridge'}; gifts['fourth'] = 'calling birds'; // 添加一个键值对 |
以JavaScript同样的方式从映射中接收值:
1 2 |
var gifts = {'first': 'partridge'}; assert(gifts['first'] == 'partridge'); |
如果你在寻找一个不在映射中的键,会在返回中获取到null:
1 2 |
var gifts = {'first': 'partridge'}; assert(gifts['fifth'] == null); |
使用 .length
来获取映射中键值对的数量:
1 2 3 |
var gifts = {'first': 'partridge'}; gifts['fourth'] = 'calling birds'; assert(gifts.length == 2); |
创建为编译时常量的映射,在映射字面量之前添加const
:
1 2 3 4 5 6 7 |
final constantMap = const { 2: 'helium', 10: 'neon', 18: 'argon', }; // constantMap[2] = 'Helium'; // 取消本行注释会导致报错 |
在Dart 2.3中,映射像列表一样支持展开运算符 (...
和 ...?
) 及collection if 和 for。有关详情及示例,参见 展开运算符提议 和 控制流集合提议。
Runes
Dart中,runes是字符串的UTF-32代码点。
Unicode为全球书写系统中的每个字母、数字和符号定义了一个独立的数值。因为Dart字符串是一个UTF-16代码单元的序列,在字符串中表达32位Unicode值要求有特殊的语法。
通常表达一个Unicode代码点的方式为 \uXXXX
,其中XXXX是一个4位数的16进制值。例如,心形字符串 (♥) 为 \u2665
。要指定比4位16进制更多或更少的位数,需将值放在花括号中。例如,笑脸emoji (😆) 为 \u{1f600}
。
String 类有多个可用于提取rune信息的属性。 codeUnitAt
和 codeUnit
属性返回16-位代码单元。使用 runes
属性来获取字符串的rune。
以下示例描述runes、16-位代码单元以及32-位代码单元之间的关系。点击 Run 来实时查看rune。
Note: 在使用列表运算符操作runes时要小心。这种方法根据具体语言、字符集和操作可能会很容易崩溃。更多相关信息,参见Stack Overflow上的 如何倒排Dart中的字符串? 。
符号
符号(Symbol) 对象表示在Dart程序中声明的一个运算符或标识符。你可能永远都不会需要用到符号,但它们对于通过名称引用标识符的 API 有无限的价值,因为最小化会修改标识符的名称但不修改标识符符号。
使用符号字面量获取一个标识符的符号,它只是#
后接标识符。
1 2 3 |
#radix #bar<code class="language-nocode"> |
符号字面量是编译时常量。
函数
Dart是一个真面向对象语言,因此即使是函数也是对象,并且拥有一个类型Function。这表示函数可赋值给变量或作为参数传递给其它函数。也可以调用将Dart类的实例看作函数来进行调用。更多详情,参见 可调用类。
以下是实现一个函数的示例:
1 2 3 |
bool isNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null; } |
虽然高效Dart推荐 为公有 API 进行类型标注,函数则省略类型时依然有效:
1 2 3 |
isNoble(atomicNumber) { return _nobleGases[atomicNumber] != null; } |
对于仅包含一个表达式的函数,可以使用简写语法:
1 |
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null; |
=> expr
语法是{ return expr; }
的简写。 =>
标记有时被称为箭头语法。
函数可以有两种类型的参数:必选和可选。必选参数放在前面,后接可选参数。可选参数可为命名参数或位置参数。
可选参数
可选参数可以为命名参数或位置参数,或者是两者都有。
命名参数
在调用一个函数时,你可以使用paramName: value
指定命名参数。例如:
1 |
enableFlags(bold: true, hidden: false); |
在定义函数时,使用 {param1, param2, …}
来指定命名参数:
1 2 |
/// 设置bold 和 hidden标记 ... void enableFlags({bool bold, bool hidden}) {...} |
虽然命名参数是一种可选参数,通过可以@required 来标记它们以表示该参数是必须 – 即用户必须为该参数提供一个值。例如:
1 |
const Scrollbar({Key key, @required Widget child}) |
如果有人尝试不指定child
参数就创建一个Scrollbar
,那么分析器会报出问题。
使用@required 标注,需要依赖meta 包并导入 package:meta/meta.dart
。
位置参数
将一组函数参数封装在 []
中,会将它们标记为可选位置参数:
1 2 3 4 5 6 7 |
String say(String from, String msg, [String device]) { var result = '$from says $msg'; if (device != null) { result = '$result with a $device'; } return result; } |
以下是无添加可选参数调用该函数的示例:
1 |
assert(say('Bob', 'Howdy') == 'Bob says Howdy'); |
以下是使用了第3个参数调用该函数的示例:
1 2 |
assert(say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal'); |
默认参数值
函数可以使用 =
来为命名参数和位置参数定义默认值。默认值必须为编译时常量。如未提供默认值,则默认值为 null
。
以下是一个为命名参数设置默认值的示例:
1 2 3 4 5 |
/// 设置bold 和 hidden标记 ... void enableFlags({bool bold = false, bool hidden = false}) {...} // bold将为true; hidden将为false enableFlags(bold: true); |
Deprecation note: 老代码中可能会使用冒号 (:
) 代替 =
来设置命名函数的默认值。原因是原来命名函数中仅支持 :
。该支持可能会被淘汰,所以我们推荐使 用=
来指定默认值。
下面的示例展示如何为位置参数设置默认值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
String say(String from, String msg, [String device = 'carrier pigeon', String mood]) { var result = '$from says $msg'; if (device != null) { result = '$result with a $device'; } if (mood != null) { result = '$result (in a $mood mood)'; } return result; } assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon'); |
也可以传递列表或映射作为默认值。以下示例定义了一个函数doStuff()
,它为 list
参数指定了一个默认列表并为 gifts
参数指定了一个默认映射。
1 2 3 4 5 6 7 8 9 10 |
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()函数
每个应用必须有一个顶级的 main()
函数,它作为应用的入口。 main()
函数返回 void
并有一个可选参数l List<String>
。
以下是一个针对web应用的 main()
函数示例:
1 2 3 4 5 |
void main() { querySelector('#sample_text_id') ..text = 'Click me!' ..onClick.listen(reverseText); } |
Note: 以下代码中 ..
语法称作一个 级联。通过级联,你可以对单个对象的成员执行多个操作。
以下是一个针对接收参数的命令行应用的main()
函数示例:
1 2 3 4 5 6 7 8 |
// 像这样运行它: 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'); } |
可以使用 args库 来定义和解析命令行参数。
函数作为一等对象
你可以传递函数来作为另一个函数的参数。例如:
1 2 3 4 5 6 7 8 |
void printElement(int element) { print(element); } var list = [1, 2, 3]; // 传递 printElement作为参数 list.forEach(printElement); |
可将函数赋值给变量,如:
1 2 |
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!'; assert(loudify('hello') == '!!! HELLO !!!'); |
该例使用了匿名函数,更多相关内容请见下一节。
匿名函数
大部分函数都是命名函数,如 main()
或 printElement()
。你也可创建一个无名称函数,称为匿名函数,有时也称为lambda 或闭包。可以将匿名函数赋值给变量,那么例如就可以从集合中添加或删除它。
匿名函数类似于命名函数 – 0或多个参数,在括号中由逗号和可选选类型标注分隔。
后面的代码块中包含函数体:
([[Type] param1[, …]]) {
codeBlock;
};
以下示例定义了一个带有无类型参数 item
的匿名函数。对列表中的第一项所调用的函数,打印包含在指定索引处的值的字符串。
1 2 3 4 |
var list = ['apples', 'bananas', 'oranges']; list.forEach((item) { print('${list.indexOf(item)}: $item'); }); |
点击 Run 执行下面的代码。
如果函数仅包含一条语句,可将其简化为使用箭头标记。将如下行拷贝到DartPad中并点击Run来验证它的功能是相同的。
1 2 |
list.forEach( (item) => print('${list.indexOf(item)}: $item')); |
词法作用域
Dart是一个词法作用域语言,表示变量的作用域仅由代码的布局静态决定。你可以“按照外部花括号” 来查看变量是否在作用域中。
以下是一个变量在各自作用域中嵌套函数的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// 返回一个将addBy添加到函数参数中的函数 Function makeAdder(num addBy) { return (num i) => addBy + i; } void main() { // 创建一个加2的函数 var add2 = makeAdder(2); // 创建一个加4的函数 var add4 = makeAdder(4); assert(add2(3) == 5); assert(add4(3) == 7); } |
测试函数是否相等
下面是一个测试顶级函数、静态方法和实例方法是否相等的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void foo() {} // 一个顶级函数 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; // 这些闭包引用相同的实例 (#2), // 因此它们相等。 assert(y.baz == x); // 这些闭包引用不同的实例, // 因此它们不相等。 assert(v.baz != w.baz); } |
返回值
所有的函数都会返回值。如果未指定返回值,语句 return null;
会隐式地附加到函数体中。
1 2 3 |
foo() {} assert(foo() == null); |
运算符
Dart定义了下表中显示的运算符。你可以重载很多运算符,在重载运算符中进行了描述。
描述 | 运算符 |
---|---|
一元后置 | expr++ expr-- () [] . ?. |
一元前置 | -expr !expr ~expr ++expr --expr |
乘除 | * / % ~/ |
加减 | + - |
按位移 | << >> >>> |
按位与 | & |
按位异或 | ^ |
按位或 | | |
关系和类型测试 | >= > <= < as is is! |
等于 | == != |
逻辑与 | && |
逻辑或 | || |
判空(null) | ?? |
条件 | expr1 ? expr2 : expr3 |
级联 | .. |
赋值 | = *= /= += -= &= ^= etc. |
在使用运算符时,可创建表达式。以下是一些运算符表达式的示例:
1 2 3 4 5 6 |
a++ a + b a = b a == b c ? a : b a is T |
在运算符表格中, 每个运算符都比下面一行的运算符优先级要高。比如乘法运算符 %
的优先级高于(因此先执行)等号运算符 ==
,而它的优先级又高于逻辑与运算符 &&
。这个优先级表示以下两行代码的执行方式相同:
1 2 3 4 5 |
// 括号提升可读性 if ((n % i == 0) && (d % i == 0)) ... // 更验阅读,但是等价的 if (n % i == 0 && d % i == 0) ... |
Warning: 对于使用两个运算项的运算符,最左侧的运算项决定使用哪个版本的运算符。例如,如果有一个Vector对象和一个Point对象, aVector + aPoint
使用Vector版本的+。
算术运算符
Dart支持常见算术运算符,如下表所示:
运算符 | 含义 |
---|---|
+ |
加 |
– |
减 |
-expr |
一元减,也称为否定(反转表达式的符号) |
* |
乘 |
/ |
除 |
~/ |
整除,返回一个整数结果 |
% |
获取整数相除的余数(模) |
示例:
1 2 3 4 5 6 7 8 |
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也同时支持前置和后置递增及递减运算符。
运算符 | 含义 |
---|---|
++var |
var = var + 1 (表达式什为 var + 1 ) |
var++ |
var = var + 1 (表达式值为 var ) |
--var |
var = var – 1 (表达式值为 var – 1 ) |
var-- |
var = var – 1 (表达式值为 var ) |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var a, b; a = 0; b = ++a; // 在b获取值之前对a递增 assert(a == b); // 1 == 1 a = 0; b = a++; // 在b获取值之后对a递增 assert(a != b); // 1 != 0 a = 0; b = --a; // 在b获取值之前对a递减 assert(a == b); // -1 == -1 a = 0; b = a--; // 在b获取值之后对a递减 assert(a != b); // -1 != 0 |
比较和关系运算符
下表中列出了比较和关系运算符的含义。
运算符 | 含义 |
---|---|
== |
等于,参见下面的讨论 |
!= |
不等于 |
> |
大于 |
< |
小于 |
>= |
大于等于 |
<= |
小于等于 |
测试两个对象x 和 y 是否表示相同的内容,使用 ==
运算符。(在极少的情况中,需要知道两个对象是否是完全相同的对象,转而使用 identical() 函数。) 以下展示 ==
运算符如何运行:
- 如果x 或 y 为null,如果两者都为null则返回true,如果只一个为null则返回false。
- 返回
x.==(y)
方法运行的结果。(没错,
==
这样的运算符是对它们的第一项所调用的方法。你甚至可以重载很多运算符,包括==
, 可参见 重载运算符。)
以下是使用每个比较和关系运算符的示例:
1 2 3 4 5 6 |
assert(2 == 2); assert(2 != 3); assert(3 > 2); assert(2 < 3); assert(3 >= 3); assert(2 <= 3); |
类型测试运算符
as
, is
和 is!
运算符对于在运行时检查类型非常方便。
运算符 | 含义 |
---|---|
as |
对象类型转换 (也用于指定 库前缀) |
is |
如果对象有指定的类型时为true |
is! |
如果对象有指定的类型时为false |
如果obj
实现了T
所指定的接口的话则 obj is T
的结果为 true。例如,obj is Object
永远为true。
使用 as
运算符将对象转换为指定类型。通常,应使用它作为对象is
测试的简写,后接使用该对象的表达式。例如,考虑使用如下代码:
1 2 3 4 |
if (emp is Person) { // 类型检查 emp.firstName = 'Bob'; } |
可以使用 as
运算符让代码更精简:
1 |
(emp as Person).firstName = 'Bob'; |
Note: 两种代码并不等同。如果 emp
为null 或者不是Person,第1个例子(带is
)不会做任何事,第二个(带as
)抛出异常。
赋值运算符
如你所见,可以使用=
运算符进行赋值。仅为值为null的变量赋值时,使用 ??=
运算符。
1 2 3 4 |
// 为a赋值 a = value; // 若b为null时为 b 赋值,否则,b 保持不变 b ??= value; |
复合赋值运算符如 +=
将运算和赋值合并在一起。
= |
–= |
/= |
%= |
>>= |
^= |
+= |
*= |
~/= |
<<= |
&= |
|= |
以下是复合赋值运算符运行的方式:
复合赋值 | 比较表达式 | |
---|---|---|
对于运算符 op: | a op= b |
a = a op b |
示例: | a += b |
a = a + b |
下例中使用赋值和复合赋值运算符:
1 2 3 |
var a = 2; // 使用 =赋值 a *= 3; // 赋值和乘法: a = a * 3 assert(a == 6); |
逻辑运算符
可以使用逻辑运算符取反或合并布尔表达式。
运算符 | 含义 |
---|---|
!expr |
对紧接着的表达式进行取反(修改false 为 true,反之亦然) |
|| |
逻辑或 |
&& |
逻辑与 |
以下是一个使用逻辑运算符的示例:
1 2 3 |
if (!done && (col == 0 || col == 3)) { // ...Do something... } |
按位及左移右移运算符
可以在Dart中操作数字的单个位。通常,会对整型使用这些按位和移动运算符。
运算符 | 含义 |
---|---|
& |
与 |
| |
或 |
^ |
异或 |
~expr |
一元按位取反(0变成1,1变成0) |
<< |
左移 |
>> |
右移 |
以下是使用按位和移动运算符的示例:
1 2 3 4 5 6 7 8 9 |
final value = 0x22; final bitmask = 0x0f; assert((value & bitmask) == 0x02); // 与 assert((value & ~bitmask) == 0x20); // 与 取反 assert((value | bitmask) == 0x2f); // 或 assert((value ^ bitmask) == 0x2d); // 异或 assert((value << 4) == 0x220); // 左移 assert((value >> 4) == 0x02); // 右移 |
条件表达式
Dart有两个运算符可以让我们简洁地运行可能会要求使用 if-else 语句的表达式:
condition ? expr1 : expr2
- 若if条件为true, 运行 expr1 (并返回它的值),否则运行并返回 expr2的值。
expr1 ?? expr2
- 右 expr1 为非空,返回其值,否则运行并返回 expr2的值。
在需要根据布尔表达式进行赋值时,考虑使用 ?:
。
1 |
var visibility = isPublic ? 'public' : 'private'; |
如果要测试布尔表达式是否为null,考虑使用 ??
。
1 |
String playerName(String name) => name ?? 'Guest'; |
前例至少可通过另外两种方式进行编写,但都没有它简洁:
1 2 3 4 5 6 7 8 9 10 11 |
// 使用?: 运算符的稍长版本 String playerName(String name) => name != null ? name : 'Guest'; // 使用if-else语句的非常长的版本 String playerName(String name) { if (name != null) { return name; } else { return 'Guest'; } } |
级联标记 (..)
级联 (..
) 允许我们对同一对象进行一系列操作。除调用函数外,你还可以对相同的对象访问字段。这通常可节省创建临时变量的步骤并让我们能编写列流畅的代码。
思考如下代码:
1 2 3 4 |
querySelector('#confirm') // 获取一个对象 ..text = 'Confirm' // 使用它的成员 ..classes.add('important') ..onClick.listen((e) => window.alert('Confirmed!')); |
第1个方法调用querySelector()
,返回一个选择器对象。紧接级联标记的代码对选择器对象进行操作,它忽略所有后续可能返回的值。
前例等同于:T
1 2 3 4 |
var button = querySelector('#confirm'); button.text = 'Confirm'; button.classes.add('important'); button.onClick.listen((e) => window.alert('Confirmed!')); |
可以内嵌级联。例如:
1 2 3 4 5 6 7 8 |
final addressBook = (AddressBookBuilder() ..name = 'jenny' ..email = 'jenny@example.com' ..phone = (PhoneNumberBuilder() ..number = '415-555-0100' ..label = 'home') .build()) .build(); |
对于返回实际对象的函数构建级联时要非常小心。例如,如下代码会失败:
1 2 3 |
var sb = StringBuffer(); sb.write('foo') ..write('bar'); // Error: method 'write' isn't defined for 'void'. |
sb.write()
调用返回void,而无法对 void
构建级联。
Note: 严格的说,对级联使用的“双点号”标记并非运算符。它只是Dart语法的一部分。
其它运算符
我们已经在其它示例中看到过大部分剩余的运算符:
运算符 | 名称 | 含义 |
---|---|---|
() |
函数应用 | 表示一个函数调用 |
[] |
列表访问 | 引用列表中所指定索引处的值 |
. |
成员访问 | 引用一个表达式的属性;例如:foo.bar 从表达式foo 中选择属性 bar 。 |
?. |
条件成员访问 | 类似 . , 但最左侧的运算项可以为null;例如: foo?.bar 从foo 中选择属性 bar ,除非 foo 为 null (这时 foo?.bar 的值为 null) |
更多有关.
, ?.
和 ..
运算符的信息,参见 类。
流程控制语句
可以使用以下方式来控制Dart 代码中的流程
if
和else
for
循环while
和do
–while
循环break
和continue
switch
和case
assert
也可以使用try-catch
和 throw
来影响控制流程,在 异常中有进行讲解。
if和else
Dart 支持带有可选的else
语句的 if
语句,如下例中所示。同时请参见 条件表达式。
1 2 3 4 5 6 7 |
if (isRaining()) { you.bringRainCoat(); } else if (isSnowing()) { you.wearJacket(); } else { car.putTopDown(); } |
不同于JavaScript,条件必须使用布尔值,其它的都不行。参见 布尔型 获取更多信息。
for循环
你可以使用标准for
循环进行迭代。例如:
1 2 3 4 |
var message = StringBuffer('Dart is fun'); for (var i = 0; i < 5; i++) { message.write('!'); } |
Dart for
循环中的装饰捕获索引的值,避免JavaScript中所发现的常见问题。例如,思考:
1 2 3 4 5 |
var callbacks = []; for (var i = 0; i < 2; i++) { callbacks.add(() => print(i)); } callbacks.forEach((c) => c()); |
会如所预期的先输出 0
再输出 1
。但对比在JavaScript中则会先打印 2
再打印 2
。
如果所迭代的对象是一个 Iterable,可以使用forEach() 方法。如果无需知道当前的迭代计数器的话使用 forEach()
是一个很好的选择:
1 |
candidates.forEach((candidate) => candidate.interview()); |
Iterable类如List 和 Set 还把持迭代的 for-in
形式:
1 2 3 4 |
var collection = [0, 1, 2]; for (var x in collection) { print(x); // 0 1 2 } |
while和do-while
while
循环在循环之前运行条件:
1 2 3 |
while (!isDone()) { doSomething(); } |
do
–while
在循环之后运行条件:
1 2 3 |
do { printLine(); } while (!atEndOfPage()); |
break和continue
使用 break
来停止循环:
1 2 3 4 |
while (true) { if (shutDownRequested()) break; processIncomingRequests(); } |
使用 continue
来跳至下一次迭代:
1 2 3 4 5 6 7 |
for (int i = 0; i < candidates.length; i++) { var candidate = candidates[i]; if (candidate.yearsExperience < 5) { continue; } candidate.interview(); } |
如果在使用列表或集这样的Iterable时可以会以不同的方式编写该示例:
1 2 3 |
candidates .where((c) => c.yearsExperience >= 5) .forEach((c) => c.interview()); |
switch和case
Dart中的switch 语句使用==
比较整型、字符串或编译时常量。 比较的对象必须都是相同类的实例(而非其子类型的),并且该类不能重载 ==
.。枚举类型 在 switch
语句中可以良好运行。
Note: Dart中的switch语句针对受限的环境中,如解释器或扫描器。
每个非空 case
从句按照规则以break
语句结束。其它结束非空 case
从名的有效方式有 continue
, throw
或 return
语句。
在没有匹配的case
从句时使用 default
从句来执行代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
语句,因此产生了一个报错:
1 2 3 4 5 6 7 8 9 10 |
var command = 'OPEN'; switch (command) { case 'OPEN': executeOpen(); // ERROR: Missing break case 'CLOSED': executeClosed(); break; } |
但是,Dart确实支持空的 case
从句允许越过的形式:
1 2 3 4 5 6 7 8 |
var command = 'CLOSED'; switch (command) { case 'CLOSED': // 空的case直接越过 case 'NOW_CLOSED': // 同时对CLOSED 和 NOW_CLOSED运行 executeNowClosed(); break; } |
如果你真的希望越过,可以使用continue
语句及一个标签:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var command = 'CLOSED'; switch (command) { case 'CLOSED': executeClosed(); continue nowClosed; // 继续在nowClosed标签处执行 nowClosed: case 'NOW_CLOSED': // 对CLOSED 和 NOW_CLOSED同时运行 executeNowClosed(); break; } |
case
从句可以有局部变量,仅在从句作用域内部可见。
断言
在开发过程中,可以使用断言语句 assert(condition, optionalMessage)
; 来在布尔条件为false时打断正常的执行。可以通过本导览找到断言语句的很多示例。以下是另外一些示例:
1 2 3 4 5 6 7 8 |
// 确保变量值为非null assert(text != null); // 确保值小于100 assert(number < 100); // 确保这是一个https链接 assert(urlString.startsWith('https')); |
要为断言关联消息,对assert
添加一个字符串作为第2个参数。
1 2 |
assert(urlString.startsWith('https'), 'URL ($urlString) should start with "https".'); |
assert
的第1个参数可以是解析为布尔值的任意表达式。如果表达式的值为true,断言成功且执行继续。如果它为false,断言失败且会抛出异常(一个AssertionError)。
断言到底做了什么呢?这取决于你所使用的工具和框架:
- Flutter在调试模式中启用断言。
- 仅针对开发的工具如 dartdevc 通常默认启用断言。
- 一些工具,如 dart 和 dart2js,支持通过命令行标记的断言:
--enable-asserts
.
在生产模式下,会忽略断言, assert
的参数不会被执行。
异常
Dart代码可以抛出和捕获异常。异常是表示预期外的事情发生时的报错。如未捕获到异常,抛出异常的 隔离(isolate)被挂起,并且通常隔离及其程序会被终止。
不同睛Java,Dart的异常都是非检查型异常。方法未声明它们所要抛出的异常,那么也不要求你捕获任何异常。
Dart提供 Exception 和 Error 类型,以及很多预定义的子类型。当然也可以定义自己的异常。但是Dart程序可抛出任意非空对象(不只是Exception和Error对象)来作为异常。
Throw
以下是一个抛出异常的示例:
1 |
throw FormatException('Expected at least 1 section'); |
你也可以抛出自己的对象:
1 |
throw 'Out of llamas!'; |
因为抛出异常是一个表达式,你可以在=>及其它允许使用表达式的任何地方抛出异常:
1 |
void distanceTo(Point other) => throw UnimplementedError(); |
Catch
捕获或抓取异常停止异常的传导(除非你重新抛出了该异常)。捕获异常提供了一个处理它的机会:
1 2 3 4 5 |
try { breedMoreLlamas(); } on OutOfLlamasException { buyMoreLlamas(); } |
处理会抛出一个类型以上异常的代码,可以指定多个捕获分分钟。第一个匹配所抛出对象类型的catch从句会处理该异常。如果catch从句未指定类型,该分分钟可处理任意类型抛出的对象:
1 2 3 4 5 6 7 8 9 10 11 12 |
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()
指定一个或两个参数。第1个是抛出的异常,第二是栈跟踪 (一个 StackTrace对象).
1 2 3 4 5 6 7 8 |
try { // ··· } on Exception catch (e) { print('Exception details:\n $e'); } catch (e, s) { print('Exception details:\n $e'); print('Stack trace:\n $s'); } |
要部分处理异常,但允许其执行,使用 rethrow
关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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
从句执行后会推出异常。
1 2 3 4 5 6 |
try { breedMoreLlamas(); } finally { // 即使在没有抛出异常时也会清理 cleanLlamaStalls(); } |
finally
从任意匹配的catch
从句之后执行:
1 2 3 4 5 6 7 |
try { breedMoreLlamas(); } catch (e) { print('Error: $e'); // 首先处理异常 } finally { cleanLlamaStalls(); // 然后进行清理 } |
阅读库导览的 异常 的一节了解更多信息。
类
Dart是一个带有类和基于mixin继承的面向对象语言。每个对象都是类的实例,所有类都继承自 Object.。 基于mixin的继承 表示虽然每个类(除Object外)都只有一个超类,一个类主体可在多个类级别中复用。
使用类成员
对象有由函数和数据(分别为方法和实例变量)组成的成员。在调用方法时,对一个对象进行调用:该方法可访问对象的函数和数据。
使用点号 (.
) 来引用于实例变量或方法:
1 2 3 4 5 6 7 8 9 10 |
var p = Point(2, 2); // 设置实例变量y的值 p.y = 3; // 获取 y 的值 assert(p.y == 3); // 对 p 调用 distanceTo() num distance = p.distanceTo(Point(4, 4)); |
使用 ?.
代替 .
来避免最左侧操作项为null时的异常:
1 2 |
// 如果p 不为null, 设置它的y 值为 4 p?.y = 4; |
使用构造函数
可以使用构造函数创建对象。构造函数名可为 ClassName
或 ClassName.identifier
。例如,如下代码使用Point()
和 Point.fromJson()
构造函数创建 Point
对象:
1 2 |
var p1 = Point(2, 2); var p2 = Point.fromJson({'x': 1, 'y': 2}); |
以下代码有同样的效果,但在构造函数名前使用可选的关键字 new
:
1 2 |
var p1 = new Point(2, 2); var p2 = new Point.fromJson({'x': 1, 'y': 2}); |
一些类提供 常量构造函数。要使用常量构造函数创建编译时常量,在构造函数名前放置关键字 const
:
1 |
var p = const ImmutablePoint(2, 2); |
构造两个相同的编译时常量会产生单个相同的实例:
1 2 3 4 |
var a = const ImmutablePoint(1, 1); var b = const ImmutablePoint(1, 1); assert(identical(a, b)); // 它们是相同的实例! |
在常量上下文中,可以在构造函数或字面量前省略 const
。例如,查看如下创建一个const映射的代码:
1 2 3 4 5 |
// 这里有很多const 关键字 const pointAndLine = const { 'point': const [const ImmutablePoint(0, 0)], 'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)], }; |
可以省略掉第一次使用之外的所有 const
关键字:
1 2 3 4 5 |
// 仅一个const, 它创建常量上下文 const pointAndLine = { 'point': [ImmutablePoint(0, 0)], 'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)], }; |
如果常量构造函数在常量上下文之外且未使用 const
调用,它创建一个非常量对象。
1 2 3 4 |
var a = const ImmutablePoint(1, 1); // 创建一个常量 var b = ImmutablePoint(1, 1); // 不创建常量 assert(!identical(a, b)); // 不是相同的实例! |
获取一个对象的类型
要在运行时获取对象的类型,可以使用Object的 runtimeType
属性,它返回一个 Type 对象。
1 |
print('The type of a is ${a.runtimeType}'); |
到这里,你已学习了如何使用类。本节后面的部分展示如何实现类。
实例变量
以下是如何声明实例变量:
1 2 3 4 5 |
class Point { num x; // 声明实例变量 x, 初始值为 null num y; // 声明 y, 初始值为 null num z = 0; // 声明 z, 初始值为 0 } |
所有未初始化的实例变量的值为 null
。
所有的实例变量生成一个getter 方法。非final实例变量也生成一个隐式setter 方法。更多详情,参见getters和setters。
1 2 3 4 5 6 7 8 9 10 11 |
class Point { num x; num y; } void main() { var point = Point(); point.x = 4; // 使用针对x 的setter方法 assert(point.x == 4); // 使用针对x 的getter方法 assert(point.y == null); // 默认值为null } |
如果在其声明处实例化实例变量(而非在构造函数或方法中进行),在实例创建时设置值,即在构造函数及期初始化程序列表执行前。
构造函数
通过和类相同的名称创建函数来声明构造函数(以及命名构造函数中所描述的可选额外标识符)。最常见形式的构造函数,生成构造函数,创建一个类的新实例:
1 2 3 4 5 6 7 8 9 |
class Point { num x, y; Point(num x, num y) { // 有更好的实现方式,请保持关注 this.x = x; this.y = y; } } |
this
关键字引用的是当前实例。
Note: 仅在有名称冲突时使用 this
。否则Dart 的样式省略 this
。
为实例变量赋值构造函数的参数非常普通 ,Dart提供了语法糖来进行简化:
1 2 3 4 5 6 7 |
class Point { num x, y; // 在构造函数体运行前用于 // 设置 x 和 y 的语法糖 Point(this.x, this.y); } |
默认构造函数
如未声明构造函数,会为你提供默认的构造函数。默认构造函数没有参数并在超类中调用无参数构造函数。
未继承构造函数
子类不会从超类中继承构造函数。未声明构造函数的子类仅拥有默认构造函数(无参数,无名称)。
命名构造函数
使用命令构造函数来为类实现多个构造函数或增强清晰度:
1 2 3 4 5 6 7 8 9 10 11 |
class Point { num x, y; Point(this.x, this.y); // 命名构造函数 Point.origin() { x = 0; y = 0; } } |
记住构造函数并不会继承,也就表示超类的构造函数不会由子类继承。如果希望通过超类中定义的命名构造函数创建子类 ,必须在子类中实现这个构造函数。
调用非默认超类构造函数
默认,子类中的构造函数调用超类中的未命名、无参数的构造函数。超类的构造函数在构造函数体的开头调用。如果还使用了 初始化程序列表,它在超类调用前执行。总之,执行的顺序如下:
- 初始化程序列表initializer list
- 超类的无参构造函数
- 主类的无参构造函数
如果超类中没有未命名的无参构造函数,那么就必须手动调用超类的一个构造函数。在冒号(:
)后、刚好在构造函数体前(如有)指定超类构造函数。
在下例中,Employee类的构造函数调用其超类Person的命名构造函数。点击 Run 来执行代码。
因为超类构造函数的参数在调用构造函数前运行,参数可以是像函数调用这样的表达式:
1 2 3 4 |
class Employee extends Person { Employee() : super.fromJson(getDefaultData()); // ··· } |
Warning: 超类构造函数的参数无法访问 this
。例如,参数可调用静态方法但无法调用实例方法。
初始化程序列表
除调用超类构造函数外,也可以在构造函数体运行之前初始化实例变量。将初始化程序以逗号分隔。
1 2 3 4 5 6 7 |
// 初如化程序列表在构造函数体运行前 // 设置实例变量 Point.fromJson(Map<String, num> json) : x = json['x'], y = json['y'] { print('In Point.fromJson(): ($x, $y)'); } |
Warning: 初始化程序的右侧无法访问 this
。
在开发过程中,可以在初始化程序列表中使用assert
来验证输入。
1 2 3 |
Point.withAssert(this.x, this.y) : assert(x >= 0) { print('In Point.withAssert(): ($x, $y)'); } |
初始化程序列表在设置final字段时非常方便。以下示例在初始化程序列表中初始化了3个final字段。点击 Run 来执行代码。
重定向构造函数
有时构造函数的目的仅是重定向到相同类中的其它构造函数。重定向构造函数体为空,构造函数调用出现在冒号(:)之后。
1 2 3 4 5 6 7 8 9 |
class Point { num x, y; // 这个类的主构造函数 Point(this.x, this.y); // 代理重定向至主构造函数 Point.alongXAxis(num x) : this(x, 0); } |
常量构造函数
如果类产生永不修改的对象,可以让这些对象成为编译时常量。此时,定义一个 const
构造函数并确保所有的实例变量为 final
。
1 2 3 4 5 6 7 8 |
class ImmutablePoint { static final ImmutablePoint origin = const ImmutablePoint(0, 0); final num x, y; const ImmutablePoint(this.x, this.y); } |
常量构造函数并不总是创建常量。更多详情,请参见 使用构造函数一节。
工厂构造函数
在实现不会一直新建类的实例的构造函数时使用factory
关键字。你不能吃,一个工厂构造函数可能会从缓存返回一个实例,或者它可能返回一个子类型的实例。
以下示例演示从缓存返回对象的工厂构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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)); } Logger._internal(this.name); void log(String msg) { if (!mute) print(msg); } } |
Note: 工厂构造函数无法访问 this
。
可以像调用其它构造函数那样调用工厂构造函数:
1 2 |
var logger = Logger('UI'); logger.log('Button clicked'); |
方法
方法是为对象提供行为的函数。
实例方法
对象的实例方法可以访问实例变量和 this
。下面示例中的 distanceTo()
方法是一个实例方法的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import 'dart:math'; class Point { num x, y; Point(this.x, this.y); num distanceTo(Point other) { var dx = x - other.x; var dy = y - other.y; return sqrt(dx * dx + dy * dy); } } |
getter和setter
getter和setter是提供对象属性的读写访问的特殊方法。记得每个实例变量有一个隐式getter,以及如若合适时的setter。可以使用get
和set
关键字通过实现getter和setter来创建额外的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Rectangle { num left, top, width, height; Rectangle(this.left, this.top, this.width, this.height); // 定义两个计算属性: right 和 bottom. num get right => left + width; set right(num value) => left = value - width; num get bottom => top + height; set bottom(num 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,可以以实例变量开始,然后通过方法封装它们,都无需修改客户端模式。
Note: 不论是否显式的定义getter递增 (++)等运算符都以预期的方式运行。 为避免任意预期的副作用,运算符仅调用getter一次,在临时变量中w 保存它的值。
抽象方法
实例、getter和setter方法可以是抽象的,定义接口并将其实现留给其它类。抽象方法仅能存在于 抽象类中。
要让方法为抽象的,使用分号 (;) 而非方法体:
1 2 3 4 5 6 7 8 9 10 11 |
abstract class Doer { // 定义实例变量和方法... void doSomething(); // 定义一个抽象方法 } class EffectiveDoer extends Doer { void doSomething() { // 提供一个实现,因此这里该方法不是抽象的... } } |
抽象类
使用 abstract
修饰符来定义一个抽象类 – 一个无法实例化的类。抽象类对于定义接口非常有用,通常伴有一些实现。如果希望抽象类为可实例化的,定义一个 工厂构造函数。
抽象类通常有 抽象方法。这下是一个声明拥有抽象方法的抽象类的示例:
1 2 3 4 5 6 7 |
// 这个类声明为抽象类, // 因此无法实例化。 abstract class AbstractContainer { // 定义构造函数、字段、方法... void updateChildren(); // 抽象方法 } |
隐式接口
每个类隐式定义包含类的所有实例成员的接口及它实现的任意接口。如果希望创建支持类B而不继承B的应用的类A,类 A 应实现 B 的 接口。
类A通过在implements
从句中通过声明它们来实现一个或多个接口,然后中提供这些接口所要求的 API。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// A person. 包含greet()的隐式接口 class Person { // 在接口中,但仅在本库中可见 final _name; // 不在接口中,因为这是一个构造函数 Person(this._name); // 在接口中 String greet(String who) => 'Hello, $who. I am $_name.'; } // 一个Person接口的实现 class Impostor implements Person { 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())); } |
这是一个指定类实现多个接口的示例:
1 |
class Point implements Comparable, Location {...} |
继承类
使用 extends
来创建子类,并用 super
来引用超类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Television { void turnOn() { _illuminateDisplay(); _activateIrSensor(); } // ··· } class SmartTelevision extends Television { void turnOn() { super.turnOn(); _bootNetworkInterface(); _initializeMemory(); _upgradeApps(); } // ··· } |
重载成员
子类可重载实例方法、getters和setters。可以使用 @override
标注来表明你想要重载一个成员:
1 2 3 4 5 |
class SmartTelevision extends Television { @override void turnOn() {...} // ··· } |
要精减类型安全的代码中方法参数或实例变量的类型,可以使用关键字 covariant
。
可重载运算符
可以重载下表中所显示的运算符。例如,如果你定义了一类Vector类,可以定义一个+
方法来对两个向量进行想加。
< |
+ |
| |
[] |
> |
/ |
^ |
[]= |
<= |
~/ |
& |
~ |
>= |
* |
<< |
== |
– |
% |
>> |
Note: 你可能流到到 !=
并不是一个可重载运算符。表达式 e1 != e2
只是针对 !(e1 == e2)
的语法糖。
以下是一个重载重载 +
和 -
运算符的类的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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); // 未显示运算符 == 和 hashCode。更多详情,参见下方文字。 // ··· } void main() { final v = Vector(2, 3); final w = Vector(2, 2); assert(v + w == Vector(4, 5)); assert(v - w == Vector(0, 1)); } |
如果重载==
,还应当重载 Object的 hashCode
getter。有关重载 ==
和 hashCode
的示例,参见 实现映射的键。
更多有关重载总的信息,参见 继承类。
noSuchMethod()
要在代码尝试使用不存在的方法或实例变量时进行监测或回应,可以重载noSuchMethod()
:
1 2 3 4 5 6 7 8 9 |
class A { // 除非你重载noSuchMethod,使用一个不存在的 // 成员会导致NoSuchMethodError报错 @override void noSuchMethod(Invocation invocation) { print('You tried to use a non-existent member: ' + '${invocation.memberName}'); } } |
你无法调用一个未实现的方法,以下情况除外:
- 接收者有一个静态类型
dynamic
。 - 接收者有一个定义未实现方法的静态类型(抽象方法没有问题),并且接收者的动态类型有一个不同于
Object
类中的对noSuchMethod()
的实现。
更多信息,参数非正式的 noSuchMethod 推送规范。
枚举类型
枚举类型,常称为枚举或enum, 是一种用于展示固定数量的常量值的特殊的类。
使用enum
使用enum
关键字声明一个枚举类型:
1 |
enum Color { red, green, blue } |
枚举中的每个值有一个 index
getter,它返回在枚举声明中以0位置开始的值。例如,第一个值的索引为0,第二个值的索引为1。
1 2 3 |
assert(Color.red.index == 0); assert(Color.green.index == 1); assert(Color.blue.index == 2); |
获取枚举中所有值的列表,可使用enum的 values
常量。
1 2 |
List<Color> colors = Color.values; assert(colors[2] == Color.blue); |
可以在switch语句中使用枚举,并如果未处理所有的枚举值时会收到警告:
1 2 3 4 5 6 7 8 9 10 11 12 |
var aColor = Color.blue; switch (aColor) { case Color.red: print('Red as roses!'); break; case Color.green: print('Green as grass!'); break; default: // 没有这条,会看到一条WARNING print(aColor); // 'Color.blue' } |
枚举类型有如下的限制:
- 不能有子类、mixin实现一个enum。
- 不能显式地实例化一个 enum。
更多信息,参见Dart语言规范。
为类添加功能: mixin
mixin是在多级类中利用类的代码的一种方式。
要使用mixin,使用 with
关键字接一个或多个mixin 名称。以下示例显示使用mixin的两个类:
1 2 3 4 5 6 7 8 9 10 11 |
class Musician extends Performer with Musical { // ··· } class Maestro extends Person with Musical, Aggressive, Demented { Maestro(String maestroName) { name = maestroName; canConduct = true; } } |
要实现一个mixin,创建继承Object和未声明构造函数的类。除非你希望让mixin和普通类一样可用,否则请使用 mixin
关键字来替代 class
。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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'); } } } |
要指定只有某些类型可以使用该mixin – 例如,这样你的mixin可以调用它未定义的方法 – 使用 on
来指定所要求的超类:
1 2 3 |
mixin MusicalPerformer on Musician { // ··· } |
类变量和方法
使用 static
关键字来实现类范围的变量和方法。
静态变量
静态变量(类变量)对类范围的状态和常量非常有用:
1 2 3 4 5 6 7 8 |
class Queue { static const initialCapacity = 16; // ··· } void main() { assert(Queue.initialCapacity == 16); } |
静态变量直到使用时才进行初始化。
Note: 本文遵循 样式指南推荐 的推荐,使用 lowerCamelCase
(首字母小写的驼峰命名法)来作为常量名。
静态方法
静态方法(类方法)不对实例进行操作,因此无法访问 this
。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import 'dart:math'; class Point { num x, y; Point(this.x, this.y); static num 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); } |
Note: 考虑对常用或广泛使用的工具和功能使用顶级函数来代替静态方法。
可以使用静态方法来作为编译时常量。例如,你可以传递一个静态方法作为对常量构造函数的参数。
泛型
如果你在查看基本数组类型List的 API 文档,实际上看到的类型是 List<E>
。<…>标记表示 List是一种泛型(或参数化)类型 – 使用拥有正式类型参数的类型。按照惯例,大部分类型变量都有单字母名称,如 E, T, S, K和 V。
为什么使用泛型?
泛型通常是类型安全所需要的,但它们有比允许你的代码运行更多的好处:
- 合理地指定泛型会产生更好的生成代码。
- 可以使用泛型来减少代码重复。
如果你想要列表仅包含字符串,可以声明它为List<String>
(将其读作“字符串列表”)。这样你、你的程序员小伙伴和你的工具都会监测到为该列表赋非字符串值可能是错误的。以下是一个示例:
1 2 3 |
var names = List<String>(); names.addAll(['Seth', 'Kathy', 'Lars']); names.add(42); // Error |
另一个使用泛型的原因是减少代码重复量。泛型让我们可以在多个类型中共享单个接口和实现,而又仍拥有静态分析的好处。例如,假设你创建接口来捕获一个对象:
1 2 3 4 |
abstract class ObjectCache { Object getByKey(String key); void setByKey(String key, Object value); } |
发现你想要该接口的字符串版本,所以创建了另一个接口:
1 2 3 4 |
abstract class StringCache { String getByKey(String key); void setByKey(String key, String value); } |
稍后,你又决定要一个该接口的数值类型版本… 读者应该明白了。
泛型可以省却创建所有这些接口的麻烦。取而代之的是,你可以创建单个接口来接收类型参数:
1 2 3 4 |
abstract class Cache<T> { T getByKey(String key); void setByKey(String key, T value); } |
在以上代码中,T是占位类型。它是一个可以看作类型的占位符,开发者可以在稍后定义这个类型。
使用集合字面量
列表、集和映射字面量可以是参数化的。参数字面量和我们所看到的其它字面量一样,只是可以在方括号(或花括号)之前添加 <type>
(针对列表和集) 或 <keyType, valueType>
(针对映射)。下面是一个使用带类型字面量的示例:
1 2 3 4 5 6 7 |
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' }; |
使用带有构造函数的参数化类型
在使用构造函数时要指定一个或多个类型,将类型放在类名后的尖括号 (<...>
) 中。例如:
1 |
var nameSet = Set<String>.from(names); |
以下代码创建一个键为整型值为View类型的映射:
1 |
var views = Map<int, View>(); |
泛型集合及它们所包含的类型
Dart的泛型是实化(reified)类型,表示它们可以在运行时携带类型信息。例如,可以测试一个集合的类型:
1 2 3 |
var names = List<String>(); names.addAll(['Seth', 'Kathy', 'Lars']); print(names is List<String>); // true |
Note: 不同的是,Java中的泛型使用的是擦除(erasure)类型,表示泛型参数在运时会被删除。在 Java 中可以测试一个对象是否为List,但无法测试它是否为 List<String>
。
限制参数化类型
在实现泛型时,你可能会希望限制它的参数的类型。可以使用 extends
来做到:
1 2 3 4 5 6 |
class Foo<T extends SomeBaseClass> { // 实现代码在这里... String toString() => "Instance of 'Foo<$T>'"; } class Extender extends SomeBaseClass {...} |
使用 SomeBaseClass
或任意其它子类作为泛型的参数都是可以的:
1 2 |
var someBaseClassFoo = Foo<SomeBaseClass>(); var extenderFoo = Foo<Extender>(); |
也可以不指定泛型参数:
1 2 |
var foo = Foo(); print(foo); // 'Foo<SomeBaseClass>'的实例 |
指定任意非SomeBaseClass
类型会导致报错:
1 |
var foo = Foo<Object>(); |
使用泛型方法
一开始Dart对泛型的支持仅限于类。一种新的语法,称为泛型方法(generic method),允许在方法和函数中使用类型参数:
1 2 3 4 5 6 |
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
指令。
库可以使用包进行分发。
使用库
使用 import
来指定一个库中的命名空间如何在另一个库的作用域中使用。例如,Dart web应用通常使用 dart:html 库,可以像这样导入:
1 |
import 'dart:html'; |
import
所要求的唯一参数是一个指定库的URI。对于内置库,URI有一个特殊的 dart:
协议。对于其它库,可以使用文件系统路径或package:
协议。 package:
协议指定由像pub工具这样的包管理器所提供的库。例如:
1 |
import 'package:test/test.dart'; |
Note: URI 表示统一资源标识符。 URL(统一资源定位符) 是一种常见的URI。
指定库的前缀
如果导入两个带有冲突标识符的库,可以对其中之一或两者指定一个前缀。例如,如果library1 和 library2 都有一个Element类,那么可以有如下这s 样的代码:
1 2 3 4 5 6 7 8 |
import 'package:lib1/lib1.dart'; import 'package:lib2/lib2.dart' as lib2; // 使用lib1中的Element Element element1 = Element(); // 使用lib2中的Element lib2.Element element2 = lib2.Element(); |
仅导入库的部分内容
如果只想要使用库的部分内容,可以选择性的导入库,例如:
1 2 3 4 5 |
// 仅导入foo import 'package:lib1/lib1.dart' show foo; // 导入除foo以外的所有名称 import 'package:lib2/lib2.dart' hide foo; |
懒加载库
延时加载(也称作懒加载) 允许web应用按照需求在需要用到库时加载库。以下是一些可能会用到延时加载的情况:
- 为减少web应用的初始启动时间。
- 执行A/B测试 – 比如尝试一种算法的可选实现。
- 加载很少使用到的功能,如可选界面和对话框。
要对库进行懒加载,必须首先使用 deferred as
导入它。
1 |
import 'package:greetings/hello.dart' deferred as hello; |
在需要该库时,使用库的标识符调用 loadLibrary()
。
1 2 3 4 |
Future greet() async { await hello.loadLibrary(); hello.printGreeting(); } |
以上代码中, await
关键字暂停代码执行直到库被加载。更多有关async
和 await
的内容,参见 异步支持。
可以对库进行多次 loadLibrary()
调用,并不会产生问题。该库只会加载一次。
使用延时加载时记住如下各点:
- 延时加载库的常量在导入文件中不是常量。记住,这些量直至延时库加载后才存在。
- 不能在导入文件中使用延时库中的类型。而是考虑将接口类型移到由延时库和导入文件所共同导入的库中。
- Dart在你使用
deferred as namespace
所定义的命名空间中隐式地插入loadLibrary()
。loadLibrary()
函数返回Future。
实现库
参见创建库软件包 来获取有关如何实现一个库软件包的建议,包括:
- 如何组织库的源码
- 如何使用
export
指令 - 何时使用
part
指令 - 何时使用
library
指令
异步支持
Dart库中有大量返回 Future 或 Stream 对象的函数。这些函数是异步的,它们在设置一个可能很耗时的操作(如 I/O)后返回,无需等待操作完成。
async
和 await
关键字支持异步编程,让你可以编写看起来类似同步代码的异步代码。
处理Future
在需要完成的Future结果时,有两个选择:
- 使用
async
和await
- 使用在库导览中所描述的Future API
使用async
和 await
的是异步代码,但和同步代码很像。例如,下面的代码使用了await
来等待异步函数的执行结果:
1 |
await lookUpVersion(); |
要使用 await
,代码必须放在一个 async
函数中,一个标记为 async
的函数中:
1 2 3 4 |
Future checkVersion() async { var version = await lookUpVersion(); // Do something with version } |
使用和 try
, catch
和 finally
来在使用了await
的代码中处理错误和清理:
1 2 3 4 5 |
try { version = await lookUpVersion(); } catch (e) { // React to inability to look up the version } |
可以在一个async
函数中多次使用 await
。例如,以下代码对函数的执行结果等待3次:
1 2 3 |
var entrypoint = await findEntrypoint(); var exitCode = await runExecutable(entrypoint, args); await flushThenExit(exitCode); |
在 await表达式 expression
中, 表达式
的值通常为Future,如若不是,那么其值自动在Future中进行封装。这个Future对象表明会返回一个对象。await表达式
的值是那么返回的对象。await表达式让执行暂停,直至对象可用。
如果你在使用 await
时得到了一个编译时错误, 确保 await
出现在async
函数中。例如,在应用的main()
函数中使用 await
, main()
函数体必须标记为 async
:
1 2 3 4 |
Future main() async { checkVersion(); print('In main: version is ${await lookUpVersion()}'); } |
声明async函数
async
函数是一个通过async
修饰符标记函数体的函数。
对函数添加 async
关键字会让其返回一个Future。例如,思考下面这个返回一个字符串的同步函数:
1 |
String lookUpVersion() => '1.0.0'; |
如果你将其修改为异步
函数, 例如因为未来实现会非常耗时 – 返回的值就是一个 – Future:
1 |
Future<String> lookUpVersion() async => '1.0.0'; |
注意函数体不需要使用Future API。 Dart在必要时会创建Future对象。如果函数不返回有用的值,设置其返回类型为 Future<void>
。
对于使用futures, async
和 await
交互入门介绍,参数 异步编程codelab.
处理流(Stream)
在需要从Stream获取值时,有两种选择:
- 使用
async
和一个异步for循环 (await for
)。 - 使用Stream API,参见 在库的导览中所述。
异步 for循环有如下的形式:
1 2 3 |
await for (varOrType identifier in expression) { // 在每次流发射值时执行 } |
expression
的值必须为 Stream类型。执行流程如下:
- 等流发射值时
- 执行 for循环体,将变量设置为所射的值
- 重复1 和 2 直到流关闭
要停止监听流,可以使用 break
或 return
语句,它会跳出for循环并取消对流的监听。
如果在实现异步for 循环时得到了一个编译时错误,确保 await for
出现在一个 async
函数中。例如,要在应用的main()
中使用异步 for循环,main()
函数体必须标记为 async
:
1 2 3 4 5 6 7 |
Future main() async { // ... await for (var request in requestServer) { handleRequest(request); } // ... } |
更多有关异步编程的信息,参见库导览的 dart:async 一节。
生成器
在需要便利地生成一系列值时,考虑使用生成器函数。Dart内置支持两种生成器函数:
要实现一个同步生成器函数,将函数体标记为 sync*
,并使用 yield
语句来传送值:
1 2 3 4 |
Iterable<int> naturalsTo(int n) sync* { int k = 0; while (k < n) yield k++; } |
要实现一个异步生成器函数,将函数体标记为 async*
, 并使用 yield
语句来传送值:
1 2 3 4 |
Stream<int> asynchronousNaturalsTo(int n) async* { int k = 0; while (k < n) yield k++; } |
如果生成器是递归的,可惟通过使用 yield*
来提升性能:
1 2 3 4 5 6 |
Iterable<int> naturalsDownFrom(int n) sync* { if (n > 0) { yield n; yield* naturalsDownFrom(n - 1); } } |
可调用类
实现 call()
方法来允许Dart类的实例可以像函数一样调用。
在下例中,WannabeFunction
类定义一个接收3个字符串并进行拼接的call()函数,每个字符串间以空格分隔,最后接感叹号。点击 Run 来执行代码。
隔离(Isolate)
大部分电脑,甚至是移动平台,都有多核 CPU。为利用这些多核,开发者的传统做法是使用共享内存线程并发运行。但共享状态并发很容易出错并导致复杂的代码。
代替线程,所有的Dart代码在isolate内运行。每个isolate有其自己的内存堆,确保没有isolate的状态可通过另一个isolate访问。
更多内容,参见:
- Dart异步编程: Isolate和事件循环
- dart:isolate API手册 包含 Isolate.spawn() 和 TransferableTypedData
- Flutter网站上的后台解析 指南
Typedef
在Dart中,函数像字符串和数值一样都是对象。typedef,或函数类型别名,给函数类型一个可在声明字段和返回类型时使用的名称。typedef在函数类型赋值给变量时保留类型信息。
思考如下未使用typedef的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class SortedCollection { Function compare; SortedCollection(int f(Object a, Object b)) { compare = f; } } // Initial, broken implementation. 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
(→ 表示返回),而compare
的类型是Function。如果我们修改代码使用显式的名称并保留类型信息,开发人员和工具都可以使用该信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
typedef Compare = int Function(Object a, Object b); class SortedCollection { Compare compare; SortedCollection(this.compare); } // Initial, broken implementation. int sort(Object a, Object b) => 0; void main() { SortedCollection coll = SortedCollection(sort); assert(coll.compare is Function); assert(coll.compare is Compare); } |
因为typedef只是别名,它们提供一种检查任意函数类型的方式。例如:
1 2 3 4 5 6 7 |
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! } |
元数据(metadata)
使用metadata来为代码提供更多信息。metadata标以字符 @
开始,后接对编译时常量的引用(如 deprecated
) 或对常量构造函数的调用。
Dart代码中有两个可用的标:@deprecated
和 @override
。 对于使用 @override
的示例,参见 继承类。以下是一个使用 @deprecated
标注的示例:
1 2 3 4 5 6 7 8 9 10 |
class Television { /// _Deprecated: 使用 [turnOn] 来代替。_ @deprecated void activate() { turnOn(); } /// 打开电视的电源 void turnOn() {...} } |
可以定义自己的metadata标。下面是定义接收两个参数的@todo标注的示例:
1 2 3 4 5 6 7 8 |
library todo; class Todo { final String who; final String what; const Todo(this.who, this.what); } |
以下是使用该 @todo 标注的示例:
1 2 3 4 5 6 |
import 'todo.dart'; @Todo('seth', 'make this do something') void doSomething() { print('do something'); } |
metadata 可以出现在库、类、typedef、类型参数、构造函数、工厂、函数、字段、参数或变量声明之前,以及放在import或export指令之前。可以在运行时使用反射来获取metadata。
注释
Dart支持单行注释、多行注释和文档注释。
单行注释
单行注释以 //
开头。 //
和行尾之间的所有内容都会被Dart编译器所忽略
1 2 3 4 |
void main() { // TODO: refactor into an AbstractLlamaGreetingFactory? print('Welcome to my Llama farm!'); } |
多行注释
多行注释以 /*
开头,并以 */
结束。 /*
和 */
之间的所有内容都被Dart编译器所忽略(除非注释是文档注释,参见下一节)。多行注释可以嵌套。
1 2 3 4 5 6 7 8 9 10 |
void main() { /* * This is a lot of work. Consider raising chickens. Llama larry = Llama(); larry.feed(); larry.exercise(); larry.clean(); */ } |
文档注释
文档注释是以///
或 /**
开头的多行或单行注释。对连续行使用///
与多行文档注释异曲同工。
在文档注释内部, Dart编译器忽略方括号所包裹之外的文件。使用方括号,可以引用类、方法、字段、顶级变量、函数和参数。方括号中的名称在文档记录的程序元素词法作用域中解析。
以下是一个带有其它类和参数的文档注释的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/// 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文档。有关如何架构注释的建议,参见 Dart文档注释指南。
总结
本页总结了Dart语言的常用特性。更多功能正在实现中,但我们预计它们不会破坏现有代码。更多信息,参见Dart语言规范 和 高效Dart.
要了解Dart核心库的更多知识,参见 Dart库导览.