A tour of the Dart language 中文翻译

前言

  • 作者:seliote
  • 日期:2020-03-05
  • 版本:Dart 2.7

本文翻译自 A tour of the Dart language,并添加了部分内容,建议直接阅读英文原文。

本文谢绝转载。

本文章主要介绍 Dart 的一些主要特性,从变量和运算符介绍到类和库,阅读这篇文章需要有一定的编程经验。Language samples 这里提供了一个更简短的入门指引,有兴趣的话可以看看。

如果需要了解更多有关库的相关知识,可以看这里 library tour

如果需要了解更多关于语言自身的细节,可以看这里 Dart language specification

贴士:你可以使用 DartPad 体验绝大部分的 Dart 特性 DartPad。如果这个网页打开后无法正常使用,可以点击这里进行 Bug 反馈 DartPad troubleshooting page

使用 VSCode 创建测试项目

  • 如果安装了 Flutter 的话就无需再安装 Dart SDK(额外安装可能会导致版本冲突),否则还需要额外安装 Dart SDK,具体下载是在这里 Dart SDK 下载地址
  • 安装 VSCode 并安装 Dart 插件。
  • 依次点击 View -> Comman Palette... 输入 Dart,选择 Dart: New Project -> Console Application -> 输入项目名称,这里输入的是 dart_tour,然后选择项目存储目录。
  • 右键 bin/main.dart -> Start Debugging 即可运行。
  • 删除 bin/main.dart 下的所有代码,这将是下文中代码运行环境。

一个基础的 Dart 程序

下面的代码使用了很多 Dart 的基础特性。

// 定义一个方法,没有指定返回类型
printInteger(int aNumber) {
  // 输出到终端
  print('The number is $aNumber.');
}

// 这是应用程序开始执行的地方,没有指定返回类型与参数
main() {
  // 定义并初始化变量
  var number = 42;
  // 调用一个方法
  printInteger(number);
}

下面是这个应用用到的一些特性讲解:

// 这是一段注释,这是单行注释,本文的注释部分有详细解释。

int,一种内置类型,Dart 还有许多其他的内置类型,比如 String List 以及 bool 等。

42,一个数字,数字是一种编译期常量。

print(),一种简便的打印输出方式。

'...'(或者 "..."),一个字符串。

$变量名称(或者 ${表达式}),字符串插值:在一个字符串中包含这种字符串插值相当于直接对插值部分进行对应的值替换,本文的字符串部分有详细解释。

main(),一个特殊的、且必需的顶层方法。这个方法是应用的入口点,下面有关于 Main 方法的详细信息。

var,一种声明一个变量但是无需显式指定类型的方式。

贴士:这个页面里代码的命名规范与 Dart style guide 里保持一致。

重要概念

学习 Dart 时,下面这些概念需要牢记:

  • 万物皆对象,对象都是类的实例,即使是数字、方法甚至是 null 都是对象,所有的对象都继承自 Object
  • 虽然 Dart 是一个强类型的语言,但是类型注解(原文是 type annotations,比如 int i 中的 int)仍然是可以省略的,因为 Dart 支持自动类型推导,比如上面的例子中,变量number 的类型被自动推导为 int,如果想要明确的表示需要一个无类型的变量时,可以使用特殊的类型 dynamic 作为类型注释。
  • Dart 支持泛型, 比如 List<String>(字符串的 List)或者 List<dynamic>(一个可以存储任何对象的 List)。
  • Dart 支持顶层方法(比如 main()),同时也支持类与对象中的方法(static 与 实例方法),也可以在一个方法内部创建另一个方法(称为嵌套方法或者局部方法)。
  • Dart 也支持顶层变量,同时也支持类与对象中的变量(static 与实例变量),实例变量常被称为域或者属性。
  • 不像 Java,Dart 没有 public protected private 这样的关键字,如果一个标识符以下划线(_)开头,那它就是对库私有的,本文下面有关于库以及可见性的详细信息。
  • 标识符可以以字母或者下划线开头,后面可以加上字母、下划线或者数字。
  • Dart 同时支持表达式(有运行时值)和语句(没有运行时值),举个例子,条件表达式 condition ? expr1 : expr2 和判断语句 if-else ,前者有返回值而后者没有。一个语句通常含有一个或多个表达式,但是表达式不能直接含有语句。
  • Dart 工具可以报告两种问题:警告和错误。警告只是说明你的代码可能有错,但是并不会阻止你运行你的代码。错误又分为编译期错误和运行期错误,编译期错误会直接阻止你运行你的代码,而运行期错误会导致代码在运行时抛出一个异常。

关键字

  • 一般关键字:assertbreakcasecatchclassconstcontinuedefaultdoelseenumextendsfalsefinalfinallyforifinisnewnullrethrowreturnsuperswitchthisthrowtruetryvarvoidwhilewith
  • 上下文关键字,只有在特定的上下文中才有特殊意义:asynchideonshowsync
  • 内置关键字,为了简化将 JavaScript 引入 Dart 的工作量才引入的:abstractascovariantdeferreddynamicexportexternalfactoryFunctiongetimplementsimportinterfacelibrarymixinoperatorpartsetstatictypedef
  • Dart 1.0 之后添加的异步相关关键字:awaityield

需要避免使用关键字作为标识符(虽然部分关键字是可以作为标识符,但是应该完全避免这样做)。

变量

创建一个变量并且实例化

var name = 'Bob';

变量保存引用,这一点很重要。一个叫做 name 的变量保存着一个值为 Bob 的类型为 String 对象的引用。

变量 name 的类型被自动推断为 String,你可以通过显式指定避免自动类型推断,如果一个变量不想被限制为只能保存特定类型的引用,可以将其设置为 dynamic 或者 Object 类型,就像这里写的这样 design guidelines,就像下面这样:

dynamic name = 'Bob';

显式声明类型的例子:

String name = 'Bob';

这个页面里的代码风格和 style guide recommendation 里推荐的方式一样,使用 var 定义局部变量。

默认值

未初始化的变量的初始值是 null,即使是数字类型默认值也是 null,因为 Dart 中万物皆对象,null 也是对象。

int lineCount;
assert(null == lineCount);

贴士:生产代码中会忽略 assert 的调用,但是开发环境中 assert(condition)conditionfalse 时将抛出一个异常。

Final 以及 Const

如果你确定不会再改变一个变量的引用,最好使用 final 或者 const 来代替 var 修饰这个变量(也会自动类型推倒),或者将这二者加在变量类型前(不使用类型推导)。final 修饰的变量只可被赋值一次,const 修饰变量则是一种编译期常量(const 修饰的变量一定是 final 的),final 修饰的顶层变量或者类中 static 变量会在第一次使用时初始化。

贴士:实例变量可以是 final 的,但是不能是 const 的,final 的实例变量必须在构造器体(构造器的 {...} 部分)开始之前初始化。即,需要通过构造器参数或者在初始化列表中(initializer list)对 final 变量进行初始化。

创建并设置 final 变量的例子:

// 没有类型注解,自动类型推断
final name = 'Bob'; 
// 有类型注解,不使用自动类型推断
final String nickname = 'Bobby'; 
// 编译期错误,final 修饰的变量只能被赋值一次
// name = 'Alice'; 

const 用在编译期常量上,如果想要一个类中的变量是 const 的,那就必须是 static 的,就像 static const 这样,当你声明一个 const 变量时,就需要将它赋值为数字、字符串这类的编译期常量或者常量之间的操作结果。

const bar = 1000000; // 压强单位(dynes/cm^2)
const double atm = 1.01325 * bar; // 一标准大气压

const 修饰符并不仅仅用于声明引用不会变化的变量,也可以用于创建 const 对象(内部的值不会变),以及声明生成 const 对象的构造器。无论是否是 const 变量,都可以引用 const 对象。

// const 的 List<dynamic>
var foo = const [];
// 编译通过,运行报错不能向不可修改 List 进行添加操作
// UnsupportedError (Unsupported operation: Cannot add to an unmodifiable list)
// foo.add('');
final bar = const [];
// 等价于 const baz = const []
const baz = [];
// 编译通过,运行报错不能向不可修改 List 进行添加操作
// UnsupportedError (Unsupported operation: Cannot add to an unmodifiable list)
// baz.add('');

当声明一个 const 变量时,就像上面的 baz 一样,可以省略创建对象的表达式中的 const,下面有详细的关于 const 的信息。

可以更改非 final 非 const 变量的引用,即使它引用着一个 const 对象。

var foo = const [];
foo = [1, 2, 3];

不能更改 const 变量的引用,比如下面这样:

const baz = [];
// 编译期错误,Constant variables can't be assigned a value.
// baz = [42]; 

Dart 2.5+ 可以用类型检查、转型、collection if 与展开操作符(...and...?)定义常量。

const Object i = 3;
// 转型
const list = [i as int];
// is 与 collection if
const map = {if (i is int) i : "int"};
// 展开操作符
const set = {if (list is List<int>) ...list}; // ...and a spread

下面的有更多关于创建常量值的信息。

内置类型

Dart 对以下类型有特殊的支持:数字、字符串、布尔、List(也被称为数组)、Set、Map、rune(用于在字符串中表示 Unicode 字符)、符号(symbols)。

上面的类型都可以用字面值来进行初始化。举个例子,'this is a string' 是一个字符串字面值,true 是一个布尔类型字面值。

因为 Dart 中的变量都是对对象的引用(也可以说是类实例的引用),通常可以用构造器初始化变量,有些内置类型有着自身的构造器,举个例子,可以使用 Map() 来创建一个 map。

数字

Dart 有两种数字类型:

int:不超过 64 位的整型数值,具体长度与平台相关。Dart 虚拟机中,值的范围是在 -263 ~ 263 - 1。Dart 编译为 JavaScript 后使用 JavaScript 的数值类型,允许的值范围在 -253 ~ 253 - 1。

double:64 位双精度浮点数,符合 IEEE 754 标准。

intdouble 都是 num 的子类,num 类型都支持基础运算符比如 + - * / 以及 abs() ceil() floor() (位操作符 >> 定义在 int 类型中),如果 num 及其子类型没有你希望拥有的特性,dart:math 库也许可以满足你的需求。

int 类型是没有小数点的数字,下面是一些 int 类型的字面值。

var x = 1;
// 16 进制以 0x 开头
var hex = 0xDEABA3F; 

如果一个数字有小数点那就是 double,下面是一些 double 类型的字面值。

var y = 1.1;
// 科学计数法形式
var exponents = 1.42e5; 

Dart 2.1 开始,int 类型在必要的时候可以自动转型为 double

// 等价于 double z = 1.0; 在 Dart 2.1 之前这将导致编译错误
double z = 1;

下面是一些数字类型与字符串之间的互相转换。

// String 转 int
var one = int.parse("1");
assert(1 == one);
// String 转 double
var onePointOne = double.parse("1.1"); 
assert(1.1 == onePointOne); // 精度是准确的,不存在精度丢失
// int 转 String
String oneAsString = 1.toString(); // 常量也是对象
assert("1" == oneAsString); // String 之间的比较用 == 就行
// double 转 String
String piAsString = 3.1415926.toString();
assert("3.1415926" == piAsString);
String piAsFixedString = 3.1415926.toStringAsFixed(2); // 固定小数位精度
assert("3.14" == piAsFixedString);

下面是 int 类型的一些位操作。

// 左位移,右补 0
assert((3 << 1) == 6); 
// 右位移,左补 0
assert((3 >> 1) == 1); 
// 位或
assert((3 | 4) == 7); 

数字字面值是编译期常量,只要操作数是编译期常量那么这个算数表达式也是编译期常量。

const msPeerSeconds = 1000;
const secondUntilRetry = 5;
const msUntilRetry = msPeerSeconds * secondUntilRetry;

字符串

Dart 中的字符串是一串 UTF-16 代码单元(code units),可以使用单引或者双引号创建字符串。Dart 中的 String 类型是不可变对象。

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.";

可以使用 ${表达式} 来对字符串进行插值,如果表达式是一个单一的标识符,那么 {} 是可以省略的,为了得到一个对象相应的字符串表示,可以调用它的 toString() 方法。

// Dart 更推荐使用单引号
var s = 'String interpolation';
assert('Dart has $s, which is very handy' == "Dart has String interpolation, " + "which is very handy"); 
assert('Dart has ${s.toUpperCase()}, which is very handy' == 'Dart has STRING INTERPOLATION, which is very handy');

贴士:== 操作符用于比较两个对象是否相等,String 对象相等的条件是有着相同的代码单元。

Dart 可以自动拼接邻近的两个字符串,效果等同于使用加号。

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

另一种创建多行字符串的方式是使用三个单引号或者三个双引号。

// 引号开始行的与结束行上一行的换行符并不会加在字符串里
var s = '''
You can create 
multi-line strings like this one.
''';
// 如果第二行字符串前面有缩进空格,会同时带到字符串里
s = """You can create
multi-line strings like this one.""";

字符串前加上前缀 r 则可以创建原生(raw)字符串,这时引号内的内容不会做任何特殊处理。

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

下面会有讲如何在字符串中表示 Unicode 字符。

字符串字面值是编译期常量,计算结果为 null,数字,字符串,布尔值等常量的插值表达式也是编译期常量。

const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';
const validConstString = '$aConstNum $aConstBool $aConstString'; // 这是一个编译期常量
const aConstList = [1, 2, 3];
// 编译期错误 In constant expressions, operands of this operator must be of type 'bool', 'num', 'String' or 'null'.
// const invalidConst = '$aConstList';
var aNUm = 0;
var aBool = true;
var aString = 'a string';
// 编译错误 Const variables must be initialized with a constant value.
// const invalidConstString = '$aNUm $aBool $aString';

Dart 也是支持正则表达式的,主要是使用 RegExp 这个类,下面是一个例子:

// r 创建原生字符串,无需转义
var regex = RegExp(r'\d+');
var input = 'In ${DateTime.now().year}, I am ${DateTime.now().year - 2010} years old.';
// 确定能匹配到
assert(regex.hasMatch(input));
// 获取匹配结果
for (var match in regex.allMatches(input)) {
  // 0 是全匹配
  print(match.group(0)); 
}
/// 2020 年输出
/// 2020
/// 10

布尔

Dart 中的 bool 类型用于代表布尔类型的值,只有布尔字面值 true false 这两个对象是布尔类型,他们都是编译期常量。

Dart 的类型安全意味着你不能使用 if (非布尔值) assert(非布尔值) 这样的表达式,而必须明确的对值进行检查。

// 检测字符串是否为空
var s = '';
assert(s.isEmpty);
// 检测是否小于等于 0
var i = 0;
assert(i <= 0);
// 检测是否为 null
var n = null;
assert(null == n);
// 检测非数字,0 / 0 不会报错
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

List

Dart 中的 List 就像其他语言中的数组,是一组有序的对象,很多人称其为 lists

Dart 中的 List 就像 JavaScript 中的数组,下面是一个例子。

var list = [1, 2, 3];

贴士:上面这个例子的类型推导为 List<int>,如果向其中再加入其他非 int 对象,静态代码分析器或者运行时将会报错或引发异常。下面的类型推导会深入探讨这个方面的内容。

List 索引从 0 开始,list.length - 1 是 List 中最后一个数据,对于 List 长度与元素获取的操作就像在 JavaScript 中一样。

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

如果需要创建一个 const List,那么需要在 List 字面值前加上 const 修饰符。

// 编译期常量 List 值
var list = const [1, 2, 3];
// 编译通过,但是运行时抛异常
// UnsupportedError (Unsupported operation: Cannot modify an unmodifiable list)
// list[1] = 1;

Dart 2.3 引入了展开运算符以及 null 判断展开运算符,这为向集合中添加多个元素提供了一种简洁的方式。

举个例子,可以使用展开运算符将一个 List 中的数据全部添加到另一个 List 中。

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

Dart 2.3 引入了集合 if 与集合 for ,这提供了一种通过条件创建集合的能力。
下面是一个使用集合 if 创建一个拥有三个或四个元素的例子。

var promoActive = DateTime.now().day / 2 == 0 ? true : false;
var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];
print(nav);

下面是一个集合 for 在为 List 添加数据前进行处理的例子。

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

下面的流程控制部分讲解了更多集合 if 集合 for 的相关操作。

还有很多操作 List 的方法,下面的泛型与集合部分有更多相关信息。

Set

Dart 中的 Set 是一组不重复数据的无序集合,Dart 支持 set 类型与 Set 字面值(Set 字面值是 Dart 2.2 才引入的)。

下面是一个创建 Set 的例子。

var halogens = {'chlorine', 'bromine', 'chlorine'};
assert(2 == halogens.length);

贴士:上面这个例子的类型推导为 List<String>,如果向其中再加入其他非 String 对象,静态代码分析器或者运行时将会报错或引发异常。下面的类型推导会深入探讨这个方面的内容。

如果需要创建一个空 Set,需要在 {} 前写上类型参数,或者显式指定 Set 变量的类型参数。

var name = <String>{};
Set<String> nickname = {};
// 这将创建一个 Map<dynamic, dynamic> 而非 Set
var username = {};

注意:Set 还是 Map?Map 的字面值定义方式与 Set 的很像,因为 Map 类型的字面量定义在 Dart 中引入的较早,所以 {} 优先识别为 Map

使用 add() 或者 addAll() 方法为一个 Set 添加元素。

var elements = <String>{};
elements.add("fluorine");
// 创建一个新 List 并添加到 Set 里
elements.addAll(["chlorine", "bromine", "astatine"]);

使用 length 获取 Set 中元素数量。

var elements = <String>{};
elements.add("fluorine");
assert(1 == elements.length);

Dart 2.3 开始,Set 也支持展开运算符,集合 if 集合 for,下面会有详细的讨论。

Map

一般来讲,Map 是一个关联键与值的对象。键与值都可以是任意类型的对象,但是每个键只能出现一次,而值可以多次出现,Dart 对于 Map 的支持提供了 Map 字面值与 Map 对象。
下面是两个使用 Map 字面值的例子。

// 自动推导为 Map<Sting, String>
var gifts = {
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};
// 自动推导为 Map<int, String>
var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon'
};

贴士:在 Map 中添加不一致的类型时,静态分析器会报错或者在运行时会抛出异常。
通过 Map 的构造器也可以构造出和上文一样的 Map 对象。

// 自动推导为 Map<dynamic, dynamic>,或者使用 var gifts = Map<String, String>(); 显式指定
var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

你可能以为创建 Map 对象的方式是 new Map() 而不是直接 Map(),从 Dart 2 开始,关键字 new 就是可选的了,更多信息下面的构造器部分会详细说明。

向 Map 中添加键值对的方式与在 JavaScript 中一样:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';

从 Map 中获取值的方式与 JavaScript 中类似:

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

如果尝试通过 Map 中不存在的键获取值,将得到一个 null 对象。

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

通过 .length 可以获取其中键值对的数量。

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

如果需要创建一个编译期常量 Map,那么需要在 Map 字面值常量前加上 const 关键字。

var gifts = const {'first': 'partridge'};
// UnsupportedError (Unsupported operation: Cannot set value in unmodifiable Map)
gifts['fourth'] = 'calling birds';

Dart 2.3 开始,Map 也支持展开运算符,集合 if 集合 for,下面会有详细的讨论。
更多的有关 Map 的信息,在泛型与 Map 部分会有详细介绍。

Runes 与 Grapheme Clusters

Dart 中 Runes 用于表示字符串中的 Unicode 码点(code point),从 Dart 2.6 开始,使用 characters 包来查看与操作用户可感知到的字符,这又称为 Grapheme Clusters。

Unicode 中每个字符、数字、符号都有一个不同的数值来表示它,Dart 的字符串是一串 UTF-16 代码单元,如果需要在字符串中使用 Unicode 码点则需要使用一种特殊的语法,一般是用 \uXXXX,其中 XXXX 是四个十六进制数字,正好十六位。举个例子,心这个字符(♥)是 \u2665。但是这些字符并不总是 16 位的,如果需要表示多与 16 位或者少于 16 位的则需要把值放在花括号里,举个例子,笑脸(😆)这个 emoji,表示的方法是 \u{1f600}

如果单独读写个别 Unicode 字符,可以使用 characters 包中在 String 上定义的一些访问器(getter)方法,返回的 Characters 对象是一个字符串的 Grapheme Clusters 序列。下面是使用 characters 包中 API 的一些例子。

首先在 Dart 包管理 处搜索 characters,找到对应的包及其版本,添加至 pubspec.yaml 的 dependencies 的节点下,比如这里是

dependencies:
  characters: ^0.5.0

然后点击 VSCode 右上角的 Get Packages 拉取依赖。

返回 main.dart 文件,继续编写代码。

var hi = 'Hi 🇩🇰';
print(hi);
print('The end of the string: ${hi.substring(hi.length - 1)}'); // 乱码
print('The last character: ${hi.characters.last}'); // 正常,输出为 `🇩🇰`

具体输出与环境有关。

更多关于 characters 包的使用可以可以看看这里 characters | Dart Package

Symbol

一个 Symbol 对象代表的是 Dart 程序中声明的一个操作符或者标识符,Symbol 这个玩意你可能永远也用不到,但是对于 API 中通过名称引用标志符来说是不可或缺的,因为混淆后标识符会改变,但是 Symbol 不会。

为了得到一个标志符的 Symbol,可以使用 Symbol 字面值,具体的使用方法就是在标识符前加一个 #

var hi = 'hello world';
var hiSymbolLiteral = #hi;

Symbol 字面值是编译器常量。

方法

Dart 是一个真正的面向对象的语言,所以即使是方法也是一个对象,有着自己的类型 Function,这意味着方法可以赋值给变量或者通过参数进行传递,同时,你也可以像调用一个方法一样调用一个类的实例,下面类一节有具体说明。

下面是一个方法的例子。

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

虽然 Dart 的最佳实践是 API 最好明确提供返回类型的类型注解,但是省略掉返回类型也是可以的。

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

对于只有一条语句的方法,可以使用下面的简写方式。

var _nobleGases = [];
isNobel(atomicNumber) => null != _nobleGases[atomicNumber];

其中 => 表达式; 语法是 {return 表达式;} 的简写,=> 符号被称为箭头(arrow)语法。

贴士:只有表达式可以用于箭头语法中,语句是不行的,举个例子,你不能在箭头语法中使用 if ... else ... 但是可以使用一个条件表达式。

方法参数有两种,必需的与可选的,必需的参数需要放在可选的参数之前,可选参数分为命名参数与位置参数两种。

贴士:一些 API,尤其是 Flutter 组件的构造器中,只使用了命名参数,甚至对一些必需参数也使用了命名参数。

可选参数

可选参数可以是命名的或者指定位置的,但是同时只能使用一个。

命名参数

当使用命名参数时,将会是 {param1, param2, param3, ...} 这种形式。

bool enableFlag({bool bold, bool hidden}){
  print('blob: $bold, hidden $hidden');
}

在调用命名参数的方法时,需要使用 paramName: value 的形式,举个例子。

enableFlag(bold: true, hidden: false);

虽然命名参数是一种可选参数,但是可以通过在参数前注解一个 @required 注明这个参数是必需的,调用者就需要提供这个参数,但是这并不是强制的,调用者不传时只会得到一个警告。@required 注解在 meta 包中,需要按照之前的方式添加依赖。

import 'package:meta/meta.dart';

void enableFlags({bool bold, @required bool hidden}) {
}

main() {
  // 实际测试不传 @required 标注的参数也并不报错,静态分析器与编译器会报 Warning
  enableFlags(); 
}

位置参数

把参数放在 [] 中就可以将其表示为可选位置参数。

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

下面是不传递可选位置参数的调用:

assert('seliote says hello' == say('seliote', 'hello'));

下面是传递了可选位置参数的:

assert('seliote says hello with a iPhone' == say('seliote', 'hello', 'iPhone'));

参数默认值

对于可选参数可以使用 = 为其赋予一个默认值,这个默认值必须是编译期常量,如果没有提供默认参数值,那它的值就会是 null

void enableFlags({bool bold = false, bool hidden = false}) {
  print('bold: $bold, hidden: $hidden');
}

main() {
  enableFlags(hidden: true); // bold 参数使用默认值
}

迭代升级贴士:许多遗留代码中使用使用冒号 : 来为命名参数设置默认值,原因是一开始只支持使用冒号的方式,但是这这种方式很快就不能用了,现在最好是用等号。

下面的例子说明如何为位置参数设置默认参数值。

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

main() {
  assert('seliote says hello with a carrier pigeon' == say('seliote', 'hello'));
}

你也可以提供 List 或者 Map 等作为默认参数,下面是一个例子:

// 默认参数必须是 const 的,所以都有修饰符 const
void doStuff({List<int> list = const [1, 2, 3], Map<String, String> map = const {'first': 'pair', 'second': 'cotton'}}) {
  print('list $list');
  print('map $map');
}

main() 方法

每个 Dart 应用(比如 web 应用)都必须有一个顶层 main() 方法,这个 main 方法会被作为应用程序的入口点,它返回 void 类型,并可以接受一个可选的 List<String> 参数。

下面是一个命令行并使用了参数的 main 方法。

// 运行方式是打开 VSCode 的命令行 cd bin/  && dart main.dart 1 test
void main(List<String> args) {
  print(args);
  assert(2 == args.length);
  assert(1 == int.parse(args[0]));
  assert('test' == args[1]);
}

有一个 args 的库可以专门用来解析命令行的参数。

方法与 first-class objects

可以把一个方法当作一个参数传递给另一个方法,举个例子:

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

void main(List<String> args) { 
  var list = [1, 2, 3];
  list.forEach(printElement);
}

也可以把一个方法赋值给变量。

// func 类型 String Function(dynamic) func
// arg.toUpperCase() 这个方法没有编译期检查,因为并不知道 arg 的类型,这里自动推断出的是 dynamic
var func = (arg) => '${arg.toUpperCase()}';
assert('HI' == func('hi'));

这个例子用了匿名方法,下节会有详细信息。

匿名方法

大部分方法都是有名称的,比如 main() 方法,但是也可以创建没有名称的方法,叫做匿名方法,或者 lambda 或者闭包。你可以将一个匿名方法赋给一个变量,举个例子,你可以在一个 List 里增加或者删除方法。

匿名方法与普通的方法看起来差不多,零个或多个被逗号分开的参数,参数类型注解也是可以省略的。

下面的伪代码是包含了方法体的。

([[Type] param1[, …]]) {
  // codeBlock;
};

下面的匿名函数的例子包含了一个无类型的参数 item,List 中的每个元素都会调用这个方法打印出元素的索引与值。

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

如果方法体只含有一条语句,还可以用箭头语法 => 简化它。

var list = ['apples', 'bananas', 'oranges'];
// 参数的括号不能省略
list.forEach((item) => print('${list.indexOf(item)}: $item')); 

作用域

Dart 是有作用域的,这就意味着变量的作用范围是静态定义的,简单来说就是它在代码中出现的位置。可以根据花括号内外来判断变量是否在作用域上。

下面是一个定义了变量的嵌套方法,可以看出变量各自的作用域。

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);
      // 不能引用之后定义的
      // assert(insideFunctionAfter);
    }

    var insideFunctionAfter = true;
  }
}

上述代码中的 nestedFunction() 方法可以引用每一层的变量,从自身到顶层。

闭包

闭包是一个 Function 对象,该 Function 对象可以访问其自身作用域中的变量,即使是是在原始的作用域之外。简而言之就是保存一个引用了方法定义处局部变量的方法,在该局部变量的作用域外通过该方法还可以访问该变量。

方法可以包含在它作用域中的变量,下面的例子中,makeAdder() 方法捕获了变量 addBy,无论返回的方法到哪里,都可以使用 addBy

// 返回一个引用了外部参数 addBy 的方法
Function makeAdder(num addBy) {
  return (num i) => addBy + i;
}

void main(List<String> args) {
  // Function add2
  var add2 = makeAdder(2);
  var add4 = makeAdder(4);
  assert(5 == add2(3));
  assert(7 == add4(3));
}

方法相等性的比较

这是一个测试顶层方法、static 方法与实例方法相等性的例子:

void foo() {} // 顶层方法

class A {
  static void bar() {} // static 方法
  void baz() {} // 实例方法
}

void main() {
  // 比较顶层方法
  var x; // x 的类型是 dynamic,如果以 var x = foo; 写的话 x 的类型就是 Function
  x = foo; // Funtion 赋值时是没有括号的
  assert(foo == x);
  // 比较 static 方法
  x = A.bar;
  assert(A.bar == x);
  // 比较实例方法
  var v = A();
  var w = A();
  var y = w;
  x = w.baz;
  assert(y.baz == x); // 同一实例同一方法的对象相等
  assert(v.baz != w.baz); // 不同实例的同一方法的对象不相等
  // 编译期错误,不能通过实例对象访问 static 方法
  // v.bar();
}

返回值

所有的方法都有返回值,如果没有显式的指明返回值,Dart 会自动在方法结束添加一句 return null;

foo() {}
assert(null == foo());

操作符

Dart 有很多操作符,其中很多都是支持重载的(override),下文的运算符重载有详细介绍。

一元后置:expr++expr--()[].?.

一元前置:-expr!expr~expr++expr--exprawait expr

乘除:*/%~/

加减:+-

位移:<<>>>>>

位与:&

位异或:^

位或:|

关系与类型测试:>=><=<asisis!

相等性:==!=

逻辑与:&&

逻辑或:||

null 测试:??

条件:expr1 ? expr2 : expr3

cascade:..

赋值:=*=/=+=-=&=^=

警告:操作符的优先性基本取决于是 Dart 解析器,如果需要权威的解答,可以看看这里 Dart language specification

当你使用操作符的时候,实际上会创建一个表达式,下面是一些例子:

class T {}

void main(List<String> args) {
  var a = 1;
  var b = 2;
  a++;
  a + b;
  a == b;
  var c = false;
  c ? a : b;
  a is T;
}

上方列出的操作符中,上面的操作符优先级高于下面的,举个例子,乘除操作符 % 的优先级是高于相等性运算符 == 的,所以 % 会先于 == 执行,== 的优先级又高于 &&,所以 == 会先于 && 执行,所以下面两个表达式的结果是一样的。

var n = 6;
var d = 7;
var i = 2;
assert(((n % i == 0) && (d % i == 0)) == (n % i == 0 && d % i == 0));

警告:如果操作符对两个操作数有不同的作用,左侧的操作数会决定操作符的实际行为。举个例子:一个 Vector 对象与 PointaVector + aPoint 实际效果是使用了 Vector+ 操作。

算数操作符

Dart 支持常用的算术运算操作符,就像下面这些。

+ 加法,- 减法,-expr 一元减法,也被称为负号(反转表达式的正负),* 乘法,/ 除法,~/ 除法,返回整型结果,% 获取整数除法的余数(取模)。

例子:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // 返回 double,需要注意整型使用 `/` 相除返回 double
assert(5 ~/ 2 == 2); // 返回 int
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)

例子:

// 可以一次定义多个相同类型的变量
var a, b;
a = 0;
b = ++a; // 在 b 获得表达式的值之间就加 1
assert(a == b); // 1 == 1;
a = 0;
b = a++; // 在 b 获得表达式的值之后再加 1
assert(a != b); // 1 != 0
a = 0;
b = --a; // 在 b 获得表达式的值之间就减 1
assert(a == b); // -1 == -1;
a = 0;
b = a--; // 在 b 获得表达式的值之后再减 1
assert(a != b); // -1 != 0

相等与关系运算符

下面列出的是相等性和关系运算符。

== 等于,下面有详细讨论,!= 不等,> 大于,< 小于,>= 大于等于,<= 小于等于。

== 可以用来测试对象 xy 是否是相等的(在很少的情况下,如果需要测试两个变量引用的对象是否是同一个,可以使用 identical() 方法),下面是 == 的比较规则:

  1. 当 x 或者 y 中有 null 时,都为 null 时返回 true,当只有其中一个是 null 时返回 false
  2. 返回 x.==(y) 的结果(是的,你没看错,像 == 这种操作符是调用的前一个操作数的,你可以对很多操作符进行重载,下面会讲到具体的细节)

下面是使用了相等与比较运算符的例子。

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

类型测试运算符

asisis! 可以在运行期很方便的测试类型。

as,类型转换,也可以用于库前缀;is,当对象是指定类型是返回 trueis!,当对象是指定类型是返回 false

obj 实现了 T 接口时 obj is T 返回 true,举个例子,obj is Object 永远返回 true

as 用于把一个对象转换为另一个指定的类型,但是只有你确定对象确实是这个类型时才能进行这个操作。举个例子:

class Person {
  String name;
}

void main(List<String> args) {
  var person = Person();
  person.name = 'seliote';
  Object object = person;
  assert('seliote' == (object as Person).name);
}

如果不确定对象是否是类型 T,那么需要在强制类型转换之前使用 is T 进行类型测试:

class T {
}

void main(List<String> args) {
  var t = T();
  Object object = t;
  if (object is T) {
    print('object is T');
  }
}

贴士:当类型并不匹配时调用 as 进行转换会抛出异常。

赋值运算符

就像上面的说的,可以使用 = 进行赋值操作,如果只想要在被赋值的对象是 null 时才进行赋值操作可以使用 ??= 运算符。

int a, b;
a = 1;
b ??= 1;
assert(1 == b);
b ??= 2; // 因为 b 此时等于 1 不为 null,所以 ??= 操作符什么也没干
assert(1 == b);

复合运算符,例如 += 为赋值运算符添加了一个操作,a op= b 等价于 a = a op b,比如 a += b 等价于 a = a + b。复合运算符有 =–=/=%=>>=^=+=*=~/=<<=&=|=

下面的例子使用了赋值运算符与复合运算符。

var a = 3;
a *= 2;
assert(6 == a);

逻辑操作符

使用逻辑操作符可以对布尔表达式进行反转与符合。

!expr 对后面的表达式结果进行反转(将 false 转为 true,反之亦然),|| 逻辑或,&& 逻辑与。

下面是使用逻辑操作符的例子:

if (!done && (col == 0 || col == 3)) {
  // 其他操作
}

位操作与位位移操作

Dart 支持进行位操作,通常情况下,你可以对 int 使用下面的操作符:&,位与;|,位或;^,位异或;~expr,一元位取反;<<,位左移;>>,位右移。

下面是位操作与位位移的例子。

final value = 0x22;
final bitmask = 0x0f;
// 位操作会返回一个新对象
assert(0x02 == (value & bitmask));
assert(0x20 == (value & ~bitmask));
assert(0x2f == (value | bitmask));
assert(0x2d == (value ^ bitmask));
assert(0x220 == (value << 4));
assert(0x2 == (value >> 4));

条件表达式

Dart 提供了两个操作符用于简化一些可能需要使用 if - else 的代码。

condition ? expr1 : expr2 如果 conditiontrue 那么表达式的计算结果是 expr1,否则计算结果是 expr2

expr1 ?? expr2expr1 不为 null 时返回 expr1 的值,否则返回 expr2 的值。

当你需要依据一个结果是布尔类型的表达式来对一个变量进行赋值时,可以考虑使用 ?: 操作符。

var isPublic = DateTime.now().day % 2 == 0;
var visibility = isPublic ? 'public' : 'private';

如果一个布尔表达式用于测试是否是 null,可以考虑使用 ?? 操作符。

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

上面的这个可以重写为下面这两个方法,但是都不够简洁。

String playerName(String name) => name != null ? name : 'Guest';
String playerName(String name) {
  if (name != null) {
    return name;
  } else {
    return 'Guest';
  }
}

Cascade 操作符

Cascades(..)用于对同一个对象连续进行多次操作,除了方法调用,也可以用于访问同一对象的多个属性。Cascade 操作符可以减少临时变量的使用使代码更加流畅。

考虑下面这样的代码(看看就好,不能直接运行):

import 'dart:html';

void main(List<String> args) {
  querySelector('#confirm') // 获取一个 Element 对象
    ..text = 'confirm' // 访问属性
    ..classes.add('important') // 访问属性
    ..onClick.listen((e) => window.alert('Confirmed!')); // 添加方法
}

第一个方法调用 querySelector() 返回了一个选择器对象,再后面 Cascade 操作符后代码的返回值都将被忽略。

前面的代码等价于((看看就好,不能直接运行)):

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

Cascade 操作符是可以嵌套的,比如下面这样:

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

需要注意 Cascade 操作符的调用对象,看下面这个例子:

// 错误,write() 返回了 void,而 Cascade 作用在了 void 上
// var sb = StringBuffer();
// sb.write('hello')
//   ..write('world');
var sb = StringBuffer()
  ..write('hello ')
  ..write('world')
  ..write('!');
assert('hello world!' == sb.toString());

贴士:严格来讲,两个点的 .. Cascade 并不是操作符,只是一种 Dart 语法而已。

其他操作符

其他的一些操作符在上面的例子里多多少少都已经出现过了。

() 方法调用符,代表调用一个方法,[] List 访问符,用于访问 List 中指定索引的值,. 成员访问符,用于在一个表达式中引用属性,举个例子 foo.bar 是从表达式 foo 中选择出 bar 属性,?. 条件成员访问符,就像 . 一样,但是左边的操作对象可以为 null,例子 foo?.bar,当 foo 不为 null 时选择出 bar 属性,但是当 foonull 时返回 null

更多关于 . ?. 以及 .. 操作符的信息,类那节会有详细的解释。

流程控制语句

你可以用下面这些语句达到流程控制的作用:

ifelse

for 循环

whiledo-while 循环

breakcontinue

switchcase

assert

try catchthrow(异常那一节会有详细的解释)

if 与 else

Dart 支持 if 语句与可选的 else 语句,就像下面例子这样:

var condition = DateTime.now().day % 5;
if (0 == condition) {
  print('condition is 0');
} else if (1 == condition) {
  print('condition is 1');
} else {
  print('condition is other');
}

不像在 JavaScript,条件必须是布尔值,其他的都不行。

For 循环

可以使用标准的 For 循环进行迭代。

var sb = StringBuffer();
for (var i = 0; i < 5; ++i) {
  sb.write('!');
}

如果 For 循环中的闭包引用了 For 索引,Dart 会捕获其值(或者说捕获了对象而不是引用?),这就避免了 JavaScripte 中的一个坑,考虑以下代码:

List<Function> callbacks = [];
for (var i = 0; i < 2; ++i) {
  callbacks.add(() => i);
}
assert(0 == callbacks[0]()); // 如果是在 JavaScript 中将输出 2
assert(1 == callbacks[1]());

上述代码的 callbacks 的索引 0 与 1 调用结果分别是 0 与 1,但是在 JavaScript 中输出都将是 2。

如果迭代的是一个 Iterable 对象,就可以对其调用 forEach() 方法,如果你不需要当前循环的计数器,forEach() 应该会是一个很好的选择。

void main(List<String> args) {
  args.forEach((arg) => print(arg));
}

Iterable 类比如 ListSet 还支持 for - in 迭代。

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

While 与 do-while

while 循环在循环前计算条件:

var isDone = false;
while (!isDone) {
  print(DateTime.now());
  isDone = DateTime.now().second % 5 == 0 ? true : false;
}

do-while 循环在循环后计算条件。

var isDone = false;
do {
  print(DateTime.now());
  isDone = DateTime.now().second % 5 == 0 ? true : false;
} while (!isDone);

Break 与 continue

使用 break 停止循环。

while (true) {
  if (DateTime.now().second % 5 == 0) {
    break;
  }
  assert(DateTime.now().second % 5 != 0);
}

使用 continue 跳出本次循环。

var i = 0;
while (i++ < 10) {
  if (i % 2 == 0) {
    continue;
  }
  assert(i % 2 != 0);
}

对于 Iterable 对象,比如 List 或者 Set 对象,上面的代码实现起来差不多是这样:

var i = [1, 2, 3, 4, 5, 6, 7, 8, 9];
i.where((e) => e % 2 != 0) // 只有返回 true 才能通过过滤进入下面的调用
    .forEach((e) {
  assert(e % 2 != 0);
});

Switch 与 case

switch 在 Dart 中使用 == 来比较 int String 等编译期常量。switch 比较的两个类必须是相同类的实例(子类是不可以的),而且不能重载 ==。枚举类在 switch 中正常工作。

贴士:Dart 中的 switch 只用于有限的环境中,比如 interpreters 与 scanners 中。

通常,非空的 case 子句都是以 break 结束的。有些情况下也可能是 continue throw 或者 return 语句。

使用 default 语句在没有 case 子句匹配时执行相应代码。

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    assert('CLOSED' == command);
    break;
  case 'OPEN':
    assert('OPEN' == command);
    break;
  default:
    print('Unknown');
}

下面的例子在 case 中省略了 break 语句,所以报错了。

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    assert('CLOSED' == command);
    break;
  // 在 case 中含有语句但是不以 break 结尾时报错
  // The last statement of the 'case' should be 'break', 'continue', 'rethrow', 'return' or 'throw'.
  // Try adding one of the required statements.
  // case 'OPEN':
  //   assert('OPEN' == command);
  default:
    print('Unknown');
}

然而,Dart 支持空的 case 子句,也就是说允许空的 case fall-through。

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    assert('CLOSED' == command);
    break;
  case 'OPEN': // 空 case fall-through
  default:
    print('Unknown');
}

如果你非常想在非空的 case 上 fall-through,Dart 也是提供了这种能力的,你可以组合 continue 与一个标签达到这个目的。

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    assert('OPEN' == command);
    continue lb_ctn;
  lb_ctn:
  case 'CLOSED':
    assert('CLOSED' == command);
    break;
  default:
    print('Unknown');
}

case 子句里也是可以有局部变量的,但是作用域仅限于这个 case 子句里。

断言

在开发环境中可以使用断言语句 assert(condition, optionalMessage); 来中断不正常的执行状态,当 condition 部分为 false 时将抛出一个异常。你可以在这篇文章中看到很多断言语句。这里有更多的例子:

// 确保变量 text 有非 null 的值
assert(text != null);
// 确保变量 number 值小于 100
assert(number < 100);
// 确保这是一个 https 链接
assert(urlString.startsWith('https'));

assert 使用第二个变量,就可以在断言失败发生异常时显示相应信息。

var urlString = 'www.google.com';
// 异常相关信息是 _AssertionError 
// 'urlString.startsWith('https')': URL (www.google.com) should start with "https".)
assert(urlString.startsWith('https'),
    'URL ($urlString) should start with "https".');

assert 的参数一可以是任何返回值是布尔类型的表达式,如果表达式的值是 true,断言成功程序继续运行,如果是 false,断言失败并抛出一个 AssertionError 异常。

断言什么时候工作,这取决于你使用的工具与框架:

  • Flutter 只有在 debug 模式下断言才是正常工作的
  • Dartdevc 这种开发专用的工具通常是默认开启断言的
  • Dart 与 dart2js 这种工具允许通过命令行加上 --enable-asserts 来开启断言

在生产环境的代码中,断言是会被忽略的,而且连值都不会被计算。

异常

Dart 代码可以捕获与抛出异常。异常预示错误,表示发生了意料之外的事情,当抛出的异常未被捕获时,引起异常的 isolate(Dart 中的线程上下文)将会被挂起,一般这种情况会造成 isolate 和应用程序终止。

与 Java 比起来,Dart 中的所有异常都可以称之为非受查异常。方法无需在签名中声明它可能会抛出的异常,你也不会被强制去捕获任何异常。

Dart 提供了 ExceptionError 类型与其许多的子类,你也可以定义自己的异常类型。然而,Dart 程序可以抛出任何非 null 的对象,而不仅仅是作为异常的 ExceptionError 对象。

Throw

下面是一个抛出异常的例子:

// 异常信息是
// FormatException (FormatException: Expected at least 1 section)
throw FormatException('Expected at least 1 section');

你也可以抛出任何对象:

// 异常信息是
// "Expected at least 1 section"
throw 'Expected at least 1 section';

贴士:生产环境中的代码抛出的异常一般都是 ExceptionError 的实现。

因为 throw 语句是一个表达式,所以可以用在 => 后或者任何允许使用语句的地方。

// 对于暂未实现的方法可以这样写
void startGame() => throw UnimplementedError();

Catch

捕获异常会阻止异常的继续传播(除非你又重新抛出了异常),捕获异常后就可以对异常进行处理。

var list = [];
try {
  list[0];
} on RangeError { // 注意这里用的关键字是 on 而不是 catch
  print('0 == list.length');
}

Dart 中的异常捕获主要是由两个关键字组成,oncatchon 用于指定捕获类型,catch 用于捕获具体的对象,二者可以单独使用,也可以一起使用。一起使用时表示捕获指定类型并捕获其对象,单独使用 on 时表示捕获指定类型,但是无需其对象,单独使用 catch 时表示捕获所有异常类型,并捕获到异常对象。

如果一段代码可能抛出一种类型以上的异常,那么可以用多个捕获语句来进行捕获,捕获顺序是从上面定义的捕获语句开始,一旦类型匹配成功就由它来进行处理,如果捕获语句后没有指定任何类型,那么这个语句就能够捕获所有被抛出的对象。

var list = [];
try {
  list[0];
} on RangeError {
  print('Range error');
} on Exception catch (e) { // 注意这里用 on 捕获类型,catch 捕获对象,只要是 Exception 或其子类都会被捕获
  print('Unknow exception $e');
} catch(e) { // 没有指定类型,就可以匹配所有类型
  print('Unknow exception');
}

catch 可以接受两个参数,第一个参数是异常对象,第二个参数是堆栈信息(StackTrace 对象)。

var list = [];
try {
  list[0];
} catch(e, s) {
  print('$e\n$s');
}

如果你想处理一个异常,但是不想阻断它的继续传播,可以使用 rethrow 语句。

void misbehave() {
  try {
    dynamic foo = true; // dynamic 才能这样,其他的编译器会直接报错
    foo++;
  } catch (e) {
    print('misbehave() handle: $e');
    rethrow;
  }
}

void main(List<String> args) {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e}.');
  }
}

Finally

如果想让一段代码无论前面是否抛了异常都要执行,可以使用 finally 语句。如果异常没有匹配到捕获的语句,那么这个异常将在 finally 执行完成后继续传播。

void main(List<String> args) {
  try {
    dynamic foo = true;
    foo++;
  } finally {
    print('Close something');
  }
}

finally 语句在匹配到的任何 catch 执行之后执行

try {
  dynamic foo = true;
  foo++;
} catch (e) {
  print('Catch $e');
} finally {
  print('Close something');
}

Dart 是一个面向对象的语言,类之间是 mixin-based 继承的(单继承,提供一个类方法给另一个类,但是逻辑上并不是它的父类,类似于包含关系)。每个对象都是类的实例,并且每个类都继承自 Object。Mixin-based 继承意味着尽管所有类(除 Object 外)都只有一个直接基类,但是类的主体是可以在多个不同层次的类中被复用的。扩展方法(Extension methods)是一种为类添加功能但是不需要通过改变它也不需要创建子类的方法。

使用类成员

类的成员分为方法与数据(分别是实例方法与实例变量)。当你调用一个方法时,你实际是在一个对象上对其进行的调用,这个方法可以访问这个对象的其他方法与数据。

使用点操作符(.)来访问实例变量或者方法。

import 'dart:math';

class Point {
  int x;
  int y;
  
  num distanceTo(int x, int y) => sqrt(pow((this.x - x), 2) + pow((this.y - y), 2));
}

void main(List<String> args) {
  var p = Point();
  p.x = 2;
  p.y = 2;
  assert(2 == p.y);
  assert(sqrt(2) == p.distanceTo(1, 1)); // 一致的精度损失
}

使用 ?. 来代替 . 可以避免当左边的操作数是 null 时引起异常。

class Point {
  int x;
  int y;
}

void main(List<String> args) {
  Point p;
  p?.y = 4; // 当 p 是 null 时什么也不做,否则按照正常的点操作符 . 来处理
  assert(null == p);
}

使用构造器

你可以使用构造器来创建一个对象,构造器的名字可以是 ClassName 或者 ClassName.identifier。举个例子,下面的代码使用了 Point()Point.fromJson() 构造函数。

从这里开始,下面的 Point 就不知道引用的哪个包里的了,虽然没法测试,但是不影响看代码

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

下面的代码与上面的效果一样,但是多用了 new 关键字。

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

版本贴士:从 Dart 2 开始 new 关键字才可以被省略。

一些类还提供了常量构造器。使用常量构造器可以用于构造编译期常量,使用时需要在常量构造器前加上 const 关键字。

var p = const ImmutablePoint(2, 2);

通过常量构造器传入相同参数构造出的两个编译期常量会是同一个实例。

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 同一个实例的测试

在一个常量上下文环境中,可以省略掉常量构造器前的 const,比如下面这个创建常量 Map 的例子:

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

你可以省略除了第一个 const 外的所有 const

// 只有第一个 const 还在,声明这是一个常量上下文
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

如果常量构造器没有使用 const 关键字而且也不是在一个常量上下文环境中,那它将创建出一个非常量对象。

var a = const ImmutablePoint(1, 1); // 创建了一个常量
var b = ImmutablePoint(1, 1); // 创建出的并不是常量
assert(!identical(a, b)); // 与上面两个都是常量的例子相对来看,这里并不是同一个实例的引用

版本贴士:常量上下文环境下可以省略常量构造器的 const 关键字是从 Dart 2 开始有的。

获取一个对象的类型

为了在运行时获取一个对象的类型,可以使用 Object 对象中定义的 runtimeType 属性,这个属性会返回一个 Type 对象。

var i = 1;
print('The type of i is ${i.runtimeType}'); // 输出 The type of i is int

直到现在,还只是停留在如何使用类上,下面我们介绍一下如何实现一个类。

实例变量

这个例子说明如何定义实例变量。

class Ponit {
  num x; // 声明实例变量 x,默认值是 null
  num y; // 声明实例变量 y,默认值是 null
  num z = 0; // 声明实例变量 z,默认值是 0
}

声明处对实例变量的初始化(而不是在构造器或者其他方法中)会在实例创建时就执行,这种初始化的执行顺序在构造器与初始化列表之前。

所有未显式初始化的实例变量值都是 null

所有实例变量都会自动生成一个 getter 访问器,非 final 的实例变量还会生成一个 setter 访问器,下面访问器部分有详细介绍。

class Ponit {
  num x;
  num y;
}

void main(List<String> args) {
  var p = Ponit();
  p.x = 4; // 使用了 x 属性的 setter 访问器
  assert(4 == p.x); // 使用了 x 属性的 getter 访问器
  assert(null == p.y); // 未初始化的 y 默认值是 null,使用了 y 属性的 getter 访问器
}

构造器

声明构造器的方式是创建一个名称和类名一样的方法(或者可以再加上一个可选的标识符作为修饰符来创建命名构造器),最常见的构造器,也就是生成构造器,会创建一个类的实例:

class Point {
  // 可以用这种方法一次声明两个变量
  num x, y;

  Point(num x, num y) {
    // 对于这种形式下面会有一种更好的办法
    this.x = x;
    this.y = y;
  }
}

Point point = Point(3, 7);

上面的 this 关键字是当前对象的引用。

贴士:只有在命名产生冲突的时候才使用 this,这时 Dart 推荐的一种风格。

通过构造器参数对实例变量进行赋值操作是很常见的,Dart 为此提供了一种语法糖:

class Point {
  num x, y;

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

Point point = Point(3, 7);

默认构造器

如果没有显式的声明一个构造器,Dart 会自动生成一个没有参数的调用基类无参构造器的默认构造器。且 Dart 只能有一个默认构造器(未命名构造器称之为默认构造器,这点很重要),多个命名构造器。

构造器不能被继承

子类是不会继承基类的构造器,如果子类没有提供显式的构造器,那么他将只会有一个无参无名的默认构造器。

命名构造器

使用命名构造器为类提供多个构造器,命名构造器也可以使构造关系更加清晰。

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

  // 命名构造器
  Point.origin() {
    x = 0;
    y = 0;
  }
}

Point point = Point.origin();

需要记住构造器是不能被继承的,这也就意味着基类的命名构造器在其子类中是没有的。如果子类想有一个与基类同名的命名构造器,那么就必须在子类中定义一个同名的。

调用基类的非默认构造器

默认情况下,子类会在构造器一开始就调用基类中无参无名的构造器。如果还用了初始化列表,初始化列表列表则会在构造器之前执行。总的来说,顺序是这样的:

  1. 初始化列表
  2. 基类无参构造器
  3. 自身的无参构造器

如果基类没有无名无参构造器,那么你就必须手动去调用一个基类的构造器了。调用的方法是在构造器名称后加上冒号(:)再指明基类的构造器名称。就像下面这样。

class Person {
  String name;

  Person.fromMap(Map<String, String> data) { // 有了命名构造器,Dart 就不会自动生成无参无名默认构造器
    print('in Person: $data');
  }
}

class Employee extends Person {
  Employee() : super.fromMap({}) { // 创建了一个自身的默认构造器,还调用基类的非默认构造器
    print('in no arg Employee');
  }

  Employee.fromMap(Map<String, String> data) : super.fromMap(data) { // 创建了一个自身的命名构造器,还调用基类的非默认构造器
    print('in arg Employee: $data');
  }
}

void main(List<String> args) {
  print('Print no arg constructor');
  var employeeNoArg = Employee(); // 调用默认构造器
  print('Print arg constructor');
  var employeeWithArg = Employee.fromMap({'gender': 'female'}); // 调用命名构造器
  print('Compare type');
  if (employeeNoArg is Person) { // 子类与基类类型比较
    employeeNoArg.name = 'seliote'; // 子类继承了基类的 getter 与 setter
    print('employeeNoArg is Person');
  }
  (employeeNoArg as Person).name = 'sayuli'; // 子类类型转换为基类
  print('employeeNoArg typecast to Person');
}

/// 输出:
/// Print no arg constructor
/// in Person: {}
/// in no arg Employee
/// Print arg constructor
/// in Person: {gender: female}
/// in arg Employee: {gender: female}
/// Compare type
/// employeeNoArg is Person
/// employeeNoArg typecast to Person

因为调用基类的构造器时传入的参数在调用基类构造器之前就已经计算出来了,所以参数可以是一个表达式,比如函数调用。

class Person {
  String name;

  Person.fromMap(Map<String, String> data) { // 有了命名构造器,Dart 就不会自动生成无参无名默认构造器
    print('in Person: $data');
  }
}

class Employee extends Person {

  String birthday = DateTime.now().toString();

  Employee() : super.fromMap({'birthday': DateTime.now().toString()}) { // 创建了一个自身的默认构造器,还调用基类的非默认构造器
    print('in no arg Employee');
  }

  // 参数无法访问实例变量或者方法
  // Only static members can be accessed in initializers.
  // Employee() : super.fromMap({'birthday': birthday}) { // 创建了一个自身的默认构造器,还调用基类的非默认构造器
  //   print('in no arg Employee');
  // }
}

void main(List<String> args) {
  print('Print no arg constructor');
  var employeeNoArg = Employee(); // 调用默认构造器
}

/// 输出
/// Print no arg constructor
/// in Person: {birthday: 2020-03-08 14:05:49.605332}
/// in no arg Employee

警告:调用基类构造器时是没有权限访问 this 的,举个例子,调用基类构造器时参数只能访问 static 方法而不是实例方法。

初始化列表

除了调用基类的构造器,你还可以使用初始化列表在构造器体执行之前对实例变量进行初始化。初始化列表内容之间用逗号分隔并放在基类构造器之前。

class Person {
  String name;

  Person.fromMap(Map<String, String> data) {
    print('in Person: $data');
  }
}

class Employee extends Person {

  DateTime birthday;

  Employee() : birthday = DateTime.now(), super.fromMap({'create_time': DateTime.now().toString()}) {
    name = 'seliote';
    print('in no arg Employee');
  }
}

void main(List<String> args) {
  var employee = Employee();
  print('${employee.name} : ${employee.birthday}');
}

/// 输出
/// in Person: {create_time: 2020-03-08 15:45:40.830085}
/// in no arg Employee
/// seliote : 2020-03-08 15:45:40.830082

警告:初始化器值部分不能访问 this

开发阶段,可以在初始化器中加上 assert 来对输入进行验证。

class Point {
  int x, y;
  // this 语法糖形式的初始化也是可以有初始化列表或者方法体的
  Point(this.x, this.y) : assert(x > 0 && y > 0, 'x and y must gather than 0') {
    print('Ponit: $x, $y');
  }
}

void main(List<String> args) {
  var point = Point(1, 1);
  try {
    point = Point(-1, 1);
  } on AssertionError catch(e) {
    print('Create point exception: ${e.message}');
  }
}

/// 输出
/// Ponit: 1, 1
/// Create point exception: x and y must gather than 0

初始化列表对于设置 final 域是很方便的,就像下面例子这样。

import 'dart:math';

class Point {
  final num x, y, distanceFromOrigin;

  // 参数使用 this 语法糖进行 final 域部分初始化,
  Point(this.x, this.y) : distanceFromOrigin = sqrt(pow(x, 2) + pow(y, 2));
}

重定向构造函数

很多时候,一个构造器所有要做的事就是调用另一个构造器,这个时候可以在冒号(:)后进行调用。

import 'dart:math';

class Point {
  final num x, y, distanceFromOrigin;
  
  // 默认构造器
  Point(this.x, this.y) : distanceFromOrigin = sqrt(pow(x, 2) + pow(y, 2));
  // 命名构造器,调用其他构造器
  Point.origin() : this(0, 0);
}

常量构造器

如果想要创建一个之后都不会再改变的对象,那么可以把这个对象变成一个编译期常量。通过定义一个 const 构造器并把所有实例变量设置为 final 即可。

class ImmutablePonit {
  // Final 的域
  final num x, y;
  // Const 构造器
  const ImmutablePonit(this.x, this.y);
}

void main(List<String> args) {
  // const 变量,const 对象
  const constImmutablePoint = const ImmutablePonit(0, 0);
  // 尝试改变 const 变量报错 Constant variables can't be assigned a value.
  // constImmutablePoint = ImmutablePonit(1, 1);
  // const 对象
  var immutablePoint = const ImmutablePonit(0, 0);
  // 尝试改变 const 对象报错 'x' can't be used as a setter because it's final.
  // immutablePoint.x = 1;
  // 可以改变非 const 变量的 const 引用对象
  immutablePoint = ImmutablePonit(1, 1);
}

常量构造器创建出的并不全都是常量,使用构造器一节有详细解释。

工厂构造器

对于不是总要生成一个新的对象的构造器可以使用 factory 关键字。举个例子,工厂构造器有可能是从缓存中返回一个对象,或者返回一个子类型。

下面的例子展示了如何从缓存中返回一个对象。

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

  // 这个变量是对库私有的,因为变量名以 _ 开头,注意是 static 的,工厂方法是访问不了 this 的
  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);
    }
  }
}

贴士:工厂方法不能访问 this

调用工厂方法和调用构造器一样,使用工厂方法可以规避掉 Dart 只能由一个默认构造器:

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

方法

实例方法和就是对对象提供的方法。

实例方法

实例方法可以访问实例变量与 this,下面例子中的 distanceTo() 就是一个实例方法的例子:

import 'dart:math';

class Point {
  num x, y;

  Point(this.x, this.y);

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

Getter 与 setter 访问器

Getter 与 setter 访问器是一种特殊的用于访问对象属性的方法,我们上面说过,每个实例变量都会有 getter,部分会有 setter。可以通过 get set 关键字,自己实现 getter 与 setter 来添加属性。

class Rectangle {
  num left, top, width, height;
  // 如果需要对一个对象的 getter 或 setter 进行重写,则需要用其他名称进行封装
  // 包私有的变量也是会生成 _ 前缀的 getter 与 setter
  String _author;

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

  // 再定义两个属性
  num get right => left + width;
  set right(num value) => left = value - width; // 注意 set 的语法
  num get bottom => top - height;
  set bottom(num value) => top = value + height;
  // 重新定义一个访问属性
  String get author => _author ?? 'seliote';
}

void main() {
  var rect = Rectangle(0, 0, 100, 100);
  assert(100 == rect.right);
  rect.right = 90;
  assert(-10 == rect.left);
  assert('seliote' == rect.author);
}

通过使用属性,可以实现良好的封装。比如想对实例变量进行修改,只需要使用方法进行包装以下 getter 与 setter 而无需修改客户端代码。

贴士:类似于自增(++)这种操作符是不受 getter 的影响的,为了避免 getter 的影响,操作符只会调用 getter 一次,然后将值保存在一个临时变量中。

抽象方法

实例方法,getter,setter 都可以是抽象的,定义一个接口,但是把具体实现的任务留给其他类来做。只有抽象类才能包含抽象方法。

如果一个方法是抽象的,那它就不会有任何方法体,并在方法签名完成后直接以分号(;)结尾。

// 定义一个抽象类
abstract class Doer {
  // 定一一个抽象方法
  void doSomething();
}

class EffectiveDoer extends Doer {

  @override
  void doSomething() {
    // 做点什么
  }
}

抽象类

使用 abstract 修饰符来定义一个抽象类,抽象类是不能够被实例化的。抽象类通常被用于定义一些含有部分默认实现的接口。如果你想要通过这个抽象类获得抽象类的实例,可以使用工厂构造器达到目的。

抽象类通常都会有抽象方法,这里有一个含有抽象方法的抽象类的例子:

// 被定义为抽象类,所以不能实例化
abstract class AbstractContainer {

  // 定义域、构造器、以及其他方法

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

隐式接口

所有类都隐式定义了包含了它所有实例成员以及它实现的接口的接口。如果你希望创建一个类 A 并支持 B 的 API,但是不继承 B,那么 A 就可以实现 B 的接口。

类实现接口的方式是将需要实现的接口放在 implements 关键字后,然后提供接口需要的 API。举个例子:

// 类定义,也隐式定义了一个接口,接口中回含有所有类方法,包括 getter setter
class Person {
  // 接口中会有 getter,因为是 final 的所以没有 setter
  final String _name ;

  // 构造方法不会被继承
  Person(this._name);

  // 接口中会有这个方法
  String greet(String who) => 'Hello $who. I am $_name.';
}

// 定义一个类,实现上面的隐式接口,但是隐式接口里的所有方法都需要重新实现,包括 getter setter
class Impostor implements Person {

  @override
  String get _name => '';

  @override
  String greet(String who) {
    return 'Hi $who. Do you know who I am?';
  }
}

// 参数接受 Person 类以及继承自 Person 类的类或者实现了 Person 接口的类
String greetBob(Person person) => person.greet('Bob');

main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

/// 输出
/// Hello Bob. I am Kathy.
/// Hi Bob. Do you know who I am?

下面是实现多个接口的例子。

class Point implements Comparable, Location {...}

继承类

使用 extends 来创建子类,子类中使用 super 来引用基类。

class Television {
  void turnOn() {
    print('Television on...');
  }
}

// 继承后使用基类实现
class SmartTelevision extends Television {
}

// 继承后覆盖基类实现
class OldTelevision extends Television {
  @override 
  void turnOn() {
    // 调用基类方法
    super.turnOn();
    print('Television too old, turn on failed');
  }
}

main() {
  print('OldTelevision turn on');
  SmartTelevision().turnOn();
  print('OldTelevision turn on');
  OldTelevision().turnOn();
}

/// 输出
/// OldTelevision turn on
/// Television on...
/// OldTelevision turn on
/// Television on...
/// Television too old, turn on failed

覆盖成员

子类可以覆盖基类的实例方法,getter,setter。你可以用 @override 标注指示明确覆盖了一个基类成员。就像上面例子中 OldTelevision 做的那样。

代码中缩小继承来的实例变量或者方法参数的类型范围是类型安全的,下面的 covariant(协变)关键字部分会详细讲解。

重载运算符

所有可以重载的运算符都在下面列了出来。举个例子,假设你定义了一个 Vector 类,你可能需要把 + 重载为两个 Vector 相加。
<+|[]>/^[]=<=~/&~>=*<<==%>>

贴士:你可能看到了 != 是不允许重载的,那是因为表达式 e1 != e2 指示 !(e1 == e2) 的语法糖。

下面是一个重载 +- 的例子,如果重载了 == 操作符,那么还需要重载 Object 继承来的 hashCode getter,举个例子。

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  // 重载 +
  Vector operator +(Vector other) => Vector(x + other.x, y + other.y);
  // 重载 -
  Vector operator -(Vector other) => Vector(x - other.x, y - other.y);

  // == 与 hashCode 
  // 注意 Object == 参数类型是 dynamic,默认的 == 只有当两个变量指向同一个对象时才返回 true
  @override 
  bool operator ==(dynamic other) {
    if (other is! Vector) {
      return false;
    }
    // dyncmic 类型可以直接赋值给任何类型
    // 但是类型不匹配时在运行时会抛出异常
    // _TypeError (type 'int' is not a subtype of type 'String')
    Vector vector = other;
    return (x == vector.x && y == vector.y);
  }

  @override
  int get hashCode {
    var result = 17;
    result = 37 * result + x.hashCode;
    result = 37 * result + y.hashCode;
    return result;
  }
}

void main(List<String> args) {
  final v1 = Vector(2, 3);
  final v2 = Vector(2, 2);
  assert(v1 + v2 == Vector(4, 5));
  assert(v1 - v2 == Vector(0, 1));
}

noSuchMethod()

可以通过覆盖 noSuchMethod() 方法来实现在对一个对象调用不存在方法时可以做出的响应。

class A {

  // Object 中定义的返回类型是 dynamic,返回类型协变无需其他操作
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ${invocation.memberName}');
  }
}

main() {
  dynamic a = A();
  a.dadada();
}

/// 输出
/// You tried to use a non-existent member: Symbol("dadada")

除非下面有至少一个条件是真的,否则是无法调用未实现的方法的:

  • 被调用者的类型是 dynamic
  • 被调用者的类型定义中有这个方法,但是没有具体实现(抽象类也是可以的),而且被调用的动态类型需要有一个不同于 ObjectnoSuchMethod() 实现(这句翻译的可能有问题,原文在这里 The receiver has a static type that defines the unimplemented method (abstract is OK), and the dynamic type of the receiver has an implemention of noSuchMethod() that’s different from the one in class Object.)

如果需要了解更多这个方面的信息,可以看看这里 github 传送门

扩展方法

扩展方法(Extension methods),Dart 2.7 加进来的新特性,可以为一个已存在的库添加新功能。你甚至可能在不知道它的存在的情况下却已经使用了它。举个例子,你可能在用 IDE 的自动补全,他会同时弹出常规方法与扩展方法的建议。

举个例子,String 类型有个方法 parseInt() 是定义在 string_apis.dart 中的。

下面 parseInt() 这里测试有问题,等待验证

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

更多关于使用与实现扩展方法的可以看这里 extension methods page

枚举类型

枚举类型,一种用于代表固定数量常量的特殊类型。

使用枚举

使用 enum 定义枚举类型。

enum Color {
  red, green, blue
}

枚举中的每个值都有一个索引的 getter,这些索引从 0 开始,举个例子,第一个值的索引是 0,第二个值的索引是 1。

enum Color {
  red, green, blue
}

void main(List<String> args) {
  assert(0 == Color.red.index);
  assert(1 == Color.green.index);
  assert(2 == Color.blue.index);
}

如果想要得到一个枚举的所有值,可以使用枚举的 values 常量。

enum Color {
  red, green, blue
}

void main(List<String> args) {
  var colors = Color.values;
  assert(Color.green == colors[1]);
}

你可以将枚举类型用在 switch 中,如果 case 子句中没有处理所有的枚举类型且没有 default 子句,将会得到一个警告。

enum Color { red, green, blue }

void main(List<String> args) {
  var c = Color.green;
  // 警告
  // Missing case clause for 'blue'.
  // Try adding a case clause for the missing constant, or adding a default clause.
  switch (c) {
    case Color.red:
      print('red');
      break;
    case Color.green:
      print('green');
      break;
  }
}

枚举类型有以下限制:

  • 不能创建枚举的子类,mix in,或者实现
  • 不能显式的实例化枚举类

更多信息可以看看 Dart language specification

使用 mixin 为类增加特性

Mixin 是一种在多层对代码进行复用的方式。使用 with 关键字带上一个或多个名称即可使用 mixin。实现 mixin 需要创建继承自 Object 并且未声明任何构造器的类,但是这个类定义需要使用 mixin 关键字而不是 class,除非你还想把 mixin 的类当作常规的类来使用。下面是一个例子。

// mixin,理解为编程技能
mixin Code {
  bool canJava = true;
  bool canDart = true;
  bool canFlutter = true;

  void writeApp() {
    if (canJava) {
      print('Write backend by Java');
    } else {
      print('Could not write backend');
    }
    if (canDart && canFlutter) {
      print('Write frontend by Dart & Flutter');
    } else {
      print('Could not write frontend');
    }
  }
}

// mixin,理解游泳技能
mixin Swim {
  void trySwim() {
    print('Try swim...');
  }
}

// 基类
class Person {
  final String name;
  Person(this.name);
}

// 是基类,有 mixin 的能力
class Employee extends Person with Code, Swim {
  Employee(String name) : super(name);
}

void main(List<String> args) {
  var e = Employee('seliote');
  e.canFlutter = false;
  e.writeApp();
  e.trySwim();
  if (e is Employee) {
    print('e is Employee');
  }
  // 类也是 mixin 类型
  if (e is Code) {
    print('e is Code');
  }
  if (e is Swim) {
    print('e is Swim');
  }
}

/// 输出
/// Write backend by Java
/// Could not write frontend
/// Try swim...
/// e is Employee
/// e is Code
/// e is Swim

还可以使用 on 关键字来限定能够使用该 mixin 的类类型,这样 mixin 就可以使用不是其自身定义的方法了。

// mixin 并限定可以使用的类类型
mixin Swim on Person {
  void trySwim() {
    // 调用限定类型 Person 上的 name getter
    print('$name try swim...');
  }
}

// 基类
class Person {
  final String name;
  Person(this.name);
}

class Employee extends Person with Swim {
  Employee(String name) : super(name);
}

// 未满足限定类型试图 mixin,得到报错
// 'Swim' can't be mixed onto 'Object' because 'Object' doesn't implement 'Person'.
// Try extending the class 'Swim'.
// class Apple with Swim {
// }

版本贴士:Dart 2.1 开始才支持 mixin 关键字,之前的代码大部分是使用 abstract class 来达到这个目的的,关于 2.1 开始的 mixin 相关变化可以看 Dart SDK changelog 或者 2.1 mixin specification

类变量与方法

使用 static 关键字来定义类级别的变量与方法。

静态变量

静态变量主要用于类自身的状态与常量相关功能。

class Queue {
  static const initialCapacity = 16;
}

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

静态变量直到第一次使用才会进行初始化。

贴士:根据 style guide recommendation 中说的,本篇中常量的名字都是 lowerCamelCase 命名方式。

静态方法

静态方法不能被具体对象直接进行调用,所以也就不能访问 this,举个例子:

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

贴士:考虑使用顶层的函数实现一些通用的工具或功能性方法,而不是使用静态方法。

静态方法可以用做编译期常量,举个例子,你可以把静态方法作为参数传入一个常量构造器

泛型

如果你看过 List 的 API 文档,就能看到它的实际类型是 List<E><...> 符号说明 List 是一个泛型(或者说是参数化)类型。习惯上,类型变量的名字都是用一个大写的字母,比如 TVU 等。

为什么使用泛型

泛型的使用是为了类型安全,但是对于我们所写的代码也可以带来以下的收获:

  • 合适的泛型类型会生成合适的代码
  • 泛型可以减少重复性代码

如果你想创建一个包含 String 的 List,你可以声明一个 List<String> 类型。泛型也使程序逻辑更加清楚,比如你试图给 List<String> 添加非 String 类型的对象,你的同事或者 IDE 之类的工具就会知道你这里写错了。

另一个使用泛型的理由是减少重复性代码。泛型可以将一个接口和它的实现应用在许多类型上面,但是 IDE 之类的工具仍然可以对代码进行正确的静态分析。举个例子,你创建了一个用来缓存 Object 类型的对象的接口:

// 创建 Object 对象缓存接口
abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object object);
}

现在你想对 String 类型的对象进行缓存,你又创建了一个类似的接口:

// 创建 String 对象缓存接口
abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String str);
}

之后,假设你又想对 num 类型的对象进行缓存...这种模板化代码真的很烦,所以你想到了一个办法。

使用一个带泛型参数的接口就可以解决上面这个问题。

// 创建泛型缓存接口
abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T t);
}

上面的代码中,T 只是相当于一个占位符,你可以认为它是一个开发者之后才会定义的类型。

使用集合字面值

List,Set 以及 Map 的字面值也都是可以泛型化的,泛型化的字面值上面的例子里已经有用过,就是那种在 [ 或者 { 前加上 <type> 或者 <keyType, valueType> 的,下面再给出一些例子。

var nameList = <String>['seliote', 'sayuli', 'leslie', 'leslie'];
var nameSet = <String>{'seliote', 'sayuli', 'leslie', 'leslie'};
var pageMap = <String, String>{
  'index.html': 'homepage',
  'robot.txt': 'Hints for web robots'
};

构造器中使用泛型

在使用构造器时如果需要指定泛型,需要将泛型放在 <...> 里加在类名之后,举个例子:

var nameList = <String>['seliote', 'sayuli', 'leslie', 'leslie'];
var personList = List<String>.from(nameList);
var map = Map<int, String>();

泛型集合与他们所含类型

Dart 的泛型参数是具体化的(reified),这也就意味着在运行时泛型参数是携带着自身信息的。举个例子,你可以对集合进行下面的测试:

var nameList = <String>['seliote', 'sayuli', 'leslie'];
assert(nameList is List<String>);

贴士:Java 的泛型靠的是擦除机制,在运行时泛型类型会被去掉。所以在 Java 中,你可以测试一个对象是不是 List,但是不能测试它是不是 List<String>

泛型类型限制

在定义一个泛型时,很多时候都会需要对泛型的类型进行限制,可以使用 extends 关键字达到这个目的。

class Foo {}

class Baz extends Foo {}

class Bar <T extends Foo> {}

void main() {
  // 报错,泛型类型不满足约束
  // var bar = Bar<String>();
  // 子类也是满足泛型类型 extends 要求的
  var bar = Bar<Baz>();
}

就像上面代码里的,extends 约束类型的子类型也是可以满足约束的。

当创建泛型类型时不指定泛型参数也是可以的,像下面这样:

// 类型为 List<dynamic> list
var list = List();

使用泛型方法

最初 Dart 的泛型是只能对类使用的,后来也支持对类方法使用泛型。

class Bar {
  T foo<T>(List<T> baz) {
    assert(baz.isNotEmpty);
    T tmp = baz[0];
    return tmp;
  }
}

foo<T> 中定义的泛型类型 T 可以用在下面这些地方:

  • foo 方法的返回值(T
  • foo 方法的参数(List<T>
  • foo 方法的局部变量(T tmp

更多关于泛型的内容可以看看这里 Using Generic Methods

库与可见性

importlibrary 指令可用于创建模块化的代码单元。库不仅仅是提供 API 的代码单元,也是一个可见性的封闭单元,以 _ 开头的标识符都只对其库内部可见。所有的 Dart 程序都是一个库,即使并没有 library 指令。

库可以以包(packages)的形式进行分发。

使用库

使用 import 指明怎么在一个库中使用另一个库的命名空间。

举个例子,Dart Web 应用基本都会使用 dart:html 这个命名空间,一般是这么引入的:

import 'dart:html';

import 所需的参数是一个指向库的 URI,对于内建的一些库,他们的 URI 都是以 dart: 开头的。如果需要使用其他包,可以使用一个文件系统的 URI 或者 package: 模式,package: 指定的库是类似于 pub tool 这种库管理工具提供的,举个例子:

import 'package:test/test.dart';

贴士:URI 是统一资源标识符的意思,URL(统一资源定位符)是 URI 中常见的一种。

指定库前缀

如果你引入的两个库存在标识符冲突,那么你可以为一个或两个都指定一个前缀。举个例子,假设 lib1 和 lib2 这两个库都有 Element 这个标识符,那你的代码可能就会像下面这样:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2; // 使用 as 指定库前缀

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

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

部分引入库

可以有选择性的引入库的一部分,举个例子:

// 只引入 html 的 Element
import 'dart:html' show Element;
// 除了 html 的 Element 外都引入
import 'dart:html' hide Element;

库的懒加载

懒加载(deferred loading or lazy loading)可以允许 web 程序需要使用的时候才加载库,这是一些可能会使用到懒加载的场景:

  • 缩短 web 应用的启动时间
  • 进行 A/B 测试,比如测试一个算法的另一种实现
  • 去加载一些很少使用到的功能,比如对于一块可选屏幕的支持或者对话框

警告:只有 dart2js 才支持懒加载,Flutter 这种 Dart 虚拟机,以及 dartdevc 都是不支持懒加载的,关于更多信息可以看看 issue #33118issue #27776

如果一个库是延迟加载的,那么的需要先使用 deferred as 来指定它是延迟加载的,下面是个例子:

import 'dart:math' deferred as mt;

之后要使用这个库的时候,在 as 指定的标识符上调用 loadLibrary() 即可,loadLibrary() 返回一个 Future

await mt.loadLibrary();

上面代码中使用了 await 关键字暂停线程运行直到库被加载进来,更多关于 async await 这类异步相关的信息,可以看看下面异步相关内容。

可以对一个库多次进行 loadLibrary() 调用,但是库只会被加载一次。

当使用延迟加载的时候,一定要记得下面几点:

  • 延迟加载的库中的常量在主文件(使用 import 进行延迟加载的文件,下同)中并不是常量,因为知道这个库被加载之前,这些所谓的常量并不存在
  • 在宿主文件中不能使用延迟加载库中的类型。如果想这么做的话,可以考虑将所有的接口类型都移动到另一个库中,然后分别被延迟加载的库和宿主文件加载
  • Dart 会隐式的为使用 deferred as namespace 的命名空间插入一个 loadLibrary()(没看懂这句话,原文是 Dart implicitly inserts loadLibrary() into the namespace that you define using deferred as namespace)

实现库

Create Library Packages 这里介绍了怎么实现一个库,包括下面的内容:

  • 怎么组织库的源码
  • 怎么使用 export 指令
  • 怎么使用 part 指令
  • 怎么使用 library 指令
  • 如何使用条件 import export 实现跨平台

异步支持

Dart 库里有许多返回 Future 或者 Stream 对象的方法,这些都是异步操作,这些方法内部设置了一些可能会很耗时的操作(比如 IO 操作)后就返回了,而不需要等待内部操作完成。

asyncawait 关键字用于支持异步操作,而且写出来的看着和同步的代码也几乎差不多。

处理 Future

如果想获取一个 Future 完成后的结果,有两种选择:

  • 使用 asyncawait
  • 使用 Future API,就像这里写的 library tour

使用 asyncawait 的代码就是异步操作,但是看着和同步的并没有什么特别大的差别,下面是一个使用 await 等待异步方法执行完成的例子:

void doSomething() {
}

// 使用 async 修饰的方法返回类型必须为 Future
Future<void> asyncMethod() async {
    await doSomething();
}

await 必须在 async 修饰的方法内才能使用,就像上面那样。

贴士:尽管 async 方法可能执行耗时的操作,但是它并不会等待这些操作完成。相反的,async 方法会一直运行直到遇到 await 修饰的表达式,详细信息可以看 这里,然后他就会返回一个 Future 对象,直到 await 表达式执行完成后才会恢复执行。

try catch finally 对于 awwait 表达式也是可以正常工作的。

void doSomething() {
  for (var i = 0; i < 100; ++i) {
    var u = i * i;
    print(u);
  }
}

Future<void> asyncMethod() async {
  try {
    await doSomething();
  } catch (e) {
    // do something
  } finally {
    // do something
  }
}

void main() {
  Future future = asyncMethod();
}

可以在一个 async 方法里多次使用 await,下面就是个例子:

String doSomething() {
  for (var i = 0; i < 100; ++i) {
    var u = i * i;
    print(u);
  }
  return DateTime.now().toString();
}

Future<void> asyncMethod() async {
  try {
    var s1 = await doSomething();
    var s2 = await doSomething();
    var s3 = await doSomething();
  } catch (e) {
    // do something
  } finally {
    // do something
  }
}

await expression 这种类型的表达式中,后面的 expression 一般是会返回一个 Future 对象,如果没有的话,那 expression 的返回结果会被自动包裹在一个 Future 对象里,这个 Future 对象承诺一定会返回一个对象,而这个对象就是 await expression 返回的对象,await 表达式会导致执行暂停直到返回的对象可用为止。

如果在使用 await 的时候被提示说是编译错误,首先需要确定一下你的 await 是不是用在 async 修饰的方法里。举个例子,如果在 main() 方法里使用 await,那么 main() 方法就必须被 async 修饰,就像这样:

void doSomething() {
}

void main() async {
  await doSomething();
}

声明异步方法

异步方法就是方法体被 async 修饰的方法。

标注为 async 会导致方法的返回值变成 Future。举个例子,考虑下面这个返回 String 的方法。

String lookupVersion() => '1.0.0';

如果你把它改成 async 方法,比如这个方法内部进行了什么耗时的操作,那这个方法的返回值也需要变为对应的 Future<String>

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

需要指出的是方法体内部不需要进行什么特殊的改动,比如返回值类型也不需要进行什么改动,Dart 会在需要的时候自动创建 Future 对象,如果方法没有返回值,需要把它的返回值改为 Future<void>

如果想进一步了解 Future asyncawait 的交互,可以看看这里 asynchronous programming codelab

处理 Stream

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

  • 使用 async 和异步循环(await for
  • 使用 Stream API,更多信息可以看看 library tour

贴士:在使用 await for 之前请确保用这种方式来处理真的会使代码变清晰,以及你是真的需要 steam 里的所有结果。举个例子,通常是不需要在 UI 事件监听器上使用 await for 的,因为 UI 框架一般会不停的给 stream 中发送事件。

异步循环的伪代码一般是下面这种形式:

await for (varOrType identifier in expression) {
  // stream 每发出一次数据就循环一次
}

上面 expression 的返回值必须是 stream,执行的顺序是这样的:

  • 等待 stream 发出数据
  • 将收到的数据值赋给前面的标识符并执行循环体
  • 重复上面两步直至 stream 关闭

如果需要中断对 stream 的监听,可以使用 break 或者 return 表达式跳出循环并取消监听。

如果在使用异步循环的时候被提示说是编译错误,首先需要确定一下你的异步循环是不是用在 async 修饰的方法里,举个例子,如果是在 main() 方法里使用异步循环,那么 main() 方法的方法体就必须被标注 async

// 生成 Stream,下面会有介绍
Stream<int> asyncYield() async* {
  for (int i = 0; i < 100; ++i) {
    yield i;
  }
}

Future<void> main() async {
  var nums = asyncYield();
  await for (var item in nums) {
    print(item);
  }
}

如果想要了解更多 Dart 内异步编程的信息,推荐阅读 library tour

生成器

当需要以懒生成方式生成一些值的时候,可以考虑使用生成器方法,Dart 内置支持两种生成器方法:

如果需要实现一个同步生成器,可以使用 sync* 标注方法体,然后使用 yield 生成数据。

Iterable<int> syncYield() sync* {
  for (var i = 0; i < 1000; ++i) {
    yield i;
  }
}

如果需要实现一个异步生成器,可以使用 async* 标注方法体,然后使用 yield 生成数据。

Stream<int> asyncYield() async* {
  for (int i = 0; i < 100; ++i) {
    yield i;
  }
}

如果需要一个递归的生成器,可以使用 yield*

// 异步递归生成器
Stream<int> asyncRecursiveYield(int start, int end) async* {
  if (start <= 1) {
    throw ArgumentError('start is $start, but start must gather than 1');
  }
  var tmp = start * start;
  if (tmp < end) {
    yield tmp;
    // 递归生成,这条语句断点并不会到,而是直接进入递归
    yield* asyncRecursiveYield(tmp, end); 
  }
}

Future<void> main() async {
  await for (var item in asyncRecursiveYield(2, 99999999)) {
    print(item);
  }
}

/// 输出
/// 4
/// 16
/// 256
/// 65536

可调用类

如果想要一个 Dart 类的对象可以像一个方法一样被调用,则可以再类中定义一个 call() 实例方法。下面是一个例子,CallableClass 类实现了 call() 方法,接收三个参数并用空格对他们进行连接,再在末尾加上一个感叹号。

class CallableClass {
  String call(String arg1, String arg2, String arg3) => '$arg1 $arg2 $arg3!';
}

void main() {
  // 注意是应用在对象上的
  assert('hi three gang!' == CallableClass()('hi', 'three', 'gang'));
}

Isolate

现在的计算机包括移动平台基本都是多核 CPU 环境,开发者一般都会使用共享内存的线程并发运行来达到更好地性能。但是因为线程间是共享状态,所以很容易造成错误或者写出很难懂的代码。

Dart 代码都运行在 Isolate 中而不是线程。每个 isolate 都有着自己的内存堆栈,这样就确保了 isolate 之间的独立且状态不能被其他 isolate 访问。

想要了解更多信息可以看看这里:

Typedef

Dart 中,方法也是对象,就像 int String 一样。typedef 也可以叫做方法别名,为一个方法类型取了一个名字,你可以用这个名字去定义一个域或者返回一个类型,一个 typedef 在被赋值给一个变量时,保存着原本的信息。

考虑下面这种没有使用 typedef 的例子:

class IntSortedCollection {
  // Function 不是泛型
  Function compare;

  // 参数类型是 int Function(int, int)
  IntSortedCollection(int cp(int i1, int i2)) {
    compare = cp;
  }
}

int sortInt(int i, int j) => 0;

void main() {
  // 构造器,传入 Function 对象
  var intSortedCollection = IntSortedCollection(sortInt);
  // 我们只知道这是一个 Function,但是是什么类型的 Function?
  assert(intSortedCollection.compare is Function);
  // 可以通过编译,但是运行时报错
  // NoSuchMethodError (NoSuchMethodError: Closure call with mismatched arguments: function 'sortInt'
  intSortedCollection.compare(1, 2, 3);
}

当通过构造器把 cp 赋值给 compare 时,cp 的类型信息就丢失了,cp 的类型是 (Object, Object) → int(其中 是返回的意思),然而 compare 的类型却只是 Function。如果我们使用 typedef 去保存方法的信息,就可以避免上面这种情况。

// 定义特定方法签名的 typedef
typedef Compare = int Function(int first, int second);

class IntSortedCollection {
  // 注意这里的类型是 Compare 而非 Function
  Compare compare;

  IntSortedCollection(this.compare);
}

int sortInt(int i, int j) => 0;

void main() {
  // 构造器,传入 Function 对象
  var intSortedCollection = IntSortedCollection(sortInt);
  assert(intSortedCollection.compare is Function);
  assert(intSortedCollection.compare is Compare);
  // 编译期报错 
  // Too many positional arguments: 2 expected, but 3 found.
  // intSortedCollection.compare(1, 2, 3);
}

贴士:目前 typedef 仅能用于方法上,我们期待能够进行相应扩展。

因为 typedef 是简单的别名,所以可以用于对任何方法进行检查,举个例子:

// typedef 支持泛型
typedef Compare<T> = int Function(T a, T b);

int sortInt(int a, int b) => a - b;
int sortObject(Object a, Object b) => 0;

void main() {
  assert(sortInt is Compare<int>);
  // 需要注意,int Function(Object a, Object b) 是类型 int Function(int a, int b)
  assert(sortObject is Compare<int>);
  // 需要注意,int Function(int a, int b) 不是类型 int Function(Object a, Object b)
  assert(sortInt is! Compare<Object>);
}

元数据

可以用元数据(metadata)为代码添加额外的信息。元数据注解以 @ 开头,后面跟着编译期常量(比如 deprecated)或者调用一个常量构造器。

有两个元数据可以用于所有的 Dart 代码,@deprecated@override@override 在类继承那节已经讲过了,现在举个 @deprecated 的例子:

class Television {
  /// _Deprecated: Use [turnOn] instead._
  @deprecated
  void activate() {
    turnOn(); // 对于废弃的 API 直接进行转发倒是个好办法
  }

  /// Turns the TV's power on.
  void turnOn() {
    print('Television is turn');
  }
}

你也可以定义自己的元数据注解,这里是一个自定义 @todo 并接受两个参数的例子。

/// 放在 bin/todo.dart 文件中
library todo;

class Todo {
  final String who;
  final String what;

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

下面是一个使用 @Todo 的例子。

/// 放在 bin/main.dart 文件中
import 'todo.dart';

@Todo('seliote', 'implement it')
void doSomething() {
  print('do something');
}

元数据可以用在库、类、typedef、泛型参数、构造器、工厂方法、方法、实例域、参数或者变量声明前,或者 import export 指令前,可以在运行期使用反射获得元数据相关信息。

注释

Dart 支持单行注释、多行注释以及文档注释

单行注释

Dart 单行注释以 // 开始,直到行结束符所有数据都会被 Dart 编译器忽略。

void main() {
  // TODO say hello
}

多行注释

多行注释以 /* 开始 */ 结束,之间除了文档注释之外的所有内容都会被 Dart 编译器忽略。多行注释是可以嵌套的。

void main() {
  /* 
  
  say hello

   */
}

文档注释

Dart 文档注释有两种单行的以 /// 开头,多行的以 /** 开头 */ 结尾,多个连续的行使用单行注释效果等同于多行注释

在文档注释中,只有在方括号内的才不会被 Dart 编译器忽略,方括号里可以引用类、类方法、类域、顶级变量、顶级方法、以及参数。方括号内的名称将会在文档的作用域内被解析。

下面是一个引用了其他类与参数的例子:

class A {
}

/// B is balabala
/// 
/// balalala is balalalalal
class B {

  String name;

  /// Feed by [A]
  void feed(A a) {
    // do something
  }
}

在生成文档是,上面的 [A] 会生成一个链接。

可以使用 documentation generation tool 来生成 HTML 的文档,生成文档的例子可以看 Dart API documentationGuidelines for Dart Doc Comments 这里介绍了文档书写的最佳实践。

总结

这个页面总结了一些 Dart 常用的特性。更多的特性正在开发中,但是我们希望能对现在已有的代码保持良好的兼容。如果想要了解更多特性,可以看看 Dart language specificationEffective Dart

如果想要了解更多关于 Dart 核心库的信息,可以看这里 A Tour of the Dart Libraries

posted @ 2020-03-05 19:32  seliote  阅读(475)  评论(0)    收藏  举报