泛型

如果你看过数组类型的API文档,你会发现有个类型为List,<…>标识着列表是带泛型的,这个泛型要求数组内的数据有某一种具体的类型。按照惯例,大多数类型变量会用单个大写字母表示,像E,T,S,K和V

为什么要使用泛型

泛型仅仅需要类型安全,但却会带来许多好处:

  • 指定泛型会让编写的代码更好
  • 使用泛型可以有效减少代码重复

如果你仅仅想要一个List里面包含字符串,你可以将其定义为 List。这样你,其他合作的程序员以及工具都可以检测到将非字符串添加到列表里是一个错误。举个例子:

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

另一个使用泛型的原因是可以减少代码重复。泛型使您可以在多种类型之间共享单个接口和实现,同时仍可以利用静态分析。举个例子,你创建了一个接口用于缓存对象:

  1. abstract class ObjectCache {
  2. Object getByKey(String key);
  3. void setByKey(String key, Object value);
  4. }

这个时候你需要一个仅仅支持字符串的版本,所以你创建了另一个接口:

  1. abstract class StringCache {
  2. String getByKey(String key);
  3. void setByKey(String key, String value);
  4. }

然后你又想要一个仅支持number类型的版本…..

泛型可以仅使用一个接口就解决你的问题:

  1. abstract class Cache<T> {
  2. T getByKey(String key);
  3. void setByKey(String key, T value);
  4. }

这个代码中,T是一个传入类型,仅仅是给后面定义的类型起占位符的作用

使用集合字面量

List,set,map字面量可以参数化。参数化字面量就跟我们平常看到的那些一样,除了或者。下面就是使用类型字面的例子:

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

在构造函数中使用参数类型

想要在构造函数中指定一种或者多种类型,在类名后面使用 <…> ,举个例子:

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

下面的代码创建了一个key是integer类型,value是View类型的map:

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

泛型集合已经它们包含的类型

Dart中的泛型类型在整个运行时中都会携带它们的类型,举个例子,你可以测试一种集合的类型:

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

注意: 如Java中的泛型是可擦除的,在运行时集合的类型将会被去除,所以在Java中,你只能测试一个对象是List的,而不能判断它是List

限制参数类型

在实现一个泛型类型时,你如果想要限制一下参数类型,你可以使用 extends

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

使用SomeBaseClass或者它的子类作为泛型类型参数都是可以的

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

如果不指定泛型参数也是可以的

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

如果指定的类型不是SomeBaseClass类型或者其子类都会报错

  1. var foo = Foo<Object>();
  2. //static analysis: error/warning

使用泛型方法

一开始泛型支持的限制仅仅在class层面,现在你可以将类型参数添加在方法或者函数之前,称之为泛型方法:

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

在first之前添加泛型参数可以允许你在下面各个地方使用泛型:

  • 返回值可以使用类型 T
  • 传入的参数可以使用类型 T
  • 内部参数可以使用类型T

类库和可见性

import 和 library可以方便帮助你构建模块以及分享代码。库中虽然没有提供专门进行可见性管理的API,但是你可以在标识符前以下划线 _ 开头,就可以使其仅在当前library内可见。每一个Dart程序都是一个library,即使它并没有使用library指令

使用 packages 可以将library分开

使用Libraries

使用 import 来在一个库内指定另一个库的命名空间

举个例子,Dart web应用通常会使用 dart:html 库,因此它们可以像下面这样导入:

  1. import 'dart:html';

import的唯一参数就是导入的库的URI。一些内置的库,都是以dart: 开头的。而一些第三方的库,你可以使用文件系统的路径以package: 开头。package: 开头指定的库通常由包管理器提供如 pub tool。举个例子:

  1. import 'package:test/test.dart';

注意: URI是统一资源标识符的标准,URL(统一资源定位符)是一种URI

指定库前缀

如果你导入两个库,这两个库的标识符冲突了,这时候你可以给其中一个库或者两个库都指定一个前缀。举个例子,如果库1和库2都有一个Element类,你可以像下面这样编写代码:

  1. import 'package:lib1/lib1.dart';
  2. import 'package:lib2/lib2.dart' as lib2;
  3. // Uses Element from lib1.
  4. Element element1 = Element();
  5. // Uses Element from lib2.
  6. lib2.Element element2 = lib2.Element();

部分导入

如果你想导入库中的一部分,你可以选择性的导入这个库,举个例子:

  1. // Import only foo.
  2. import 'package:lib1/lib1.dart' show foo;
  3. // Import all names EXCEPT foo.
  4. import 'package:lib2/lib2.dart' hide foo;

懒加载库

延迟加载(也叫做懒加载)允许一个web应用在某个库需要的时候再加载这个库,下面的是你可以使用的延迟加载的情况:

  • 用于减少web应用初始化的时间
  • 提供A/B测试
  • 一些很少会使用到的功能

注意: 只有Dart2js支持延迟加载, Dart虚拟机并不支持

想要懒加载一个库,你必须在import之前使用 deferred as

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

当你需要库执行loadLibrary()时,使用库的标识符

  1. Future greet() async {
  2. await hello.loadLibrary();
  3. hello.printGreeting();
  4. }

上面这段代码中,await关键字会暂停程序的执行直到library导入后
多次调用loadLibrary()并不会有问题,library仅会导入一次
在你使用延迟加载的时候,一定要记得以下的概念:

  • 延迟Library中的常量并不是导入Library文件中的常量,这些常量只有在延迟Library被导入后才会存在
  • 你不能在导入文件中使用延迟Library的类型。作为替换,考虑将接口类型移到由延迟库和导入文件导入的库中。
  • 在你使用deferred as namespace时Dart会隐式将loadLibrary()方法插入到namespace中。这个loadLibrary()方法将会返回Future类型的值

异步支持

Dart库对于返回Future和Stream的函数提供了充分的支持。这些函数都是异步的。它们在耗时操作的代码设置后就返回了,并不需要等到耗时操作完成

async 和 await 关键字支持了异步编程,让你可以像编写同步代码一样编写异步代码

处理Futures

当你需要一个已经完成的Future时,你有两个选择:

  • 使用async 和 await
  • 使用Future API

使用 async 和 await 关键字编程,代码就会像同步代码一样。举个例子,下面的代码展示了使用await去等待从一个异步函数中获取结果:

  1. await lookUpVersion();

要使用await,则代码必须在一个async函数中,这个函数被用async标记:

  1. Future checkVersion() async {
  2. var version = await lookUpVersion();
  3. // Do something with version
  4. }

注意: 尽管异步函数中可能会执行耗时操作的函数,但是它不会等待耗时操作完成。作为替换,异步函数仅仅会在第一次遇到await表达式之前执行。它返回一个Future对象,这个Future对象仅在await表达式完成之后才会恢复执行

使用 try,catch和finally来处理在使用await时遇到的问题:

  1. try {
  2. version = await lookUpVersion();
  3. } catch (e) {
  4. // React to inability to look up the version
  5. }

你可以在一个async函数中多次使用await。举个例子,下面的代码在获取函数结果时等待了三次:

  1. var entrypoint = await findEntrypoint();
  2. var exitCode = await runExecutable(entrypoint, args);
  3. await flushThenExit(exitCode);

在await表达式中,表达式的值通常都是一个Future。如果它不是,它的值也会自动被包装到一个Future对象中。Future对象会约定返回一个对象,这个返回的对象就是await表达式的值。await表达式会让执行暂停直到返回值可用时

如果你在使用await时遇到编译错误,请确认await使用在async函数中了。举个例子,在main函数中使用await,那么你的main函数主体必须被标记为async:

  1. Future main() async {
  2. checkVersion();
  3. print('In main: version is ${await lookUpVersion()}');
  4. }

声明异步函数

函数体用async进行标识的函数称之为异步函数

往函数中添加async关键字会让这个函数返回Future对象。举个例子,观察下面的这个同步函数,它返回一个字符串:

  1. String lookUpVersion() => '1.0.0';

如果你将其转为异步函数,举个例子,因为Future的实现需要消耗时间,所以它的返回值是一个Future:

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

记住函数体中并不需要使用Future API。Dart会在需要的时候自动创建Future对象。如果你的函数并不需要返回一个有用的值,让它的返回值类型为Future

Handling Streams

当你需要从Stream中获取值时,你有两个选择:

  • 使用async和异步for循环(await for)
  • 使用Stream API

注意: 在使用await for之前,请确定你真的想要所有的stream的结果。举个例子,你通常并不需要在UI事件的监听中使用await for,因为UI框架会无止尽的发送事件stream

一个异步循环通常包含下面的代码块:

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

表达式的值只能为Stream类型。这样的好处有下面这些:

  1. 直到stream返回一个值之前等待
  2. 执行for循环的主体,并将变量的值设置为emit的值
  3. 重复1和2,直到stream关闭

生成器

当你需要lazy生成值序列,你可以考虑使用生成器函数。Dart提供了两种内建类型的生成器:

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

要实现一个同步生成器函数,在函数体前使用 sync* 标识,使用yield代码块来传递数据:

  1. Iterable<int> naturalsTo(int n) sync* {
  2. int k = 0;
  3. while (k < n) yield k++;
  4. }

要实现一个异步生成器函数,在函数体前使用 sync* 标识,使用yield代码来传递数据:

  1. Stream<int> asynchronousNaturalsTo(int n) async* {
  2. int k = 0;
  3. while (k < n) yield k++;
  4. }

如果你的生成器是递归的,你可以使用 yield* 来提高它的性能

  1. Iterable<int> naturalsDownFrom(int n) sync* {
  2. if (n > 0) {
  3. yield n;
  4. yield* naturalsDownFrom(n - 1);
  5. }
  6. }

可调用类

你可以通过实现call()方法,来允许像调用函数一样调用Dart类的实例

在下面的这个例子中,WannabeFunction类将会定义一个call方法,这个方法接收三个字符串参数并把他们组合在一起,通过空格分割,用一个感叹号结尾

  1. class WannabeFunction {
  2. String call(String a, String b, String c) => '$a $b $c!';
  3. }
  4. var wf = WannabeFunction();
  5. var out = wf('Hi', 'there,', 'gang');
  6. main() => print(out);

Isolates

大多数的电脑,甚至现在的手机平台都会有多核的CPU。开发者们通常会在多个CPU中分享多个线程的内存状态。但是这样做非常容易导致错误,并且写出来的代码通常非常的复杂

不同于线程,所有的Dart代码运行在isolate内部。每一个isolate都有它自己的内存,并且确保别的isolate不能访问到当前isolate的内存状态

Typedefs

类型别名,通常我们称之为 typedef,因为它声明的时候使用关键字typedef,这是一种给类型取别名的简单方法。下面就是一个简单的例子使用类型别名来声明一个别名叫 IntList:

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


使用类型别名的时候可以使用泛型类型参数:

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

在大多数时候,推荐使用内联函数类型去替代在函数上使用类型别名。但是函数上使用类型别名也是可以用的

  1. typedef Compare<T> = int Function(T a, T b);
  2. int sort(int a, int b) => a - b;
  3. void main() {
  4. assert(sort is Compare<int>); // True!
  5. }

注解

在你的代码上使用注解可以为你的代码添加更多的信息。注解以符号 @ 开头,跟在后面的是编译时常量的引用,比如 deprecated,或者别的常量构造函数

所有代码都支持的两种注解 @deprecated 和 @override 。下面是deprecated注解的使用例子

  1. class Television {
  2. /// _Deprecated: Use [turnOn] instead._
  3. @deprecated
  4. void activate() {
  5. turnOn();
  6. }
  7. /// Turns the TV's power on.
  8. void turnOn() {...}
  9. }

你可以定义你自己注解。下面是一个定义 todo 注解的例子,它接收两个参数:

  1. library todo;
  2. class Todo {
  3. final String who;
  4. final String what;
  5. const Todo(this.who, this.what);
  6. }

下面是使用 todo 注解的例子:

  1. import 'todo.dart';
  2. @Todo('seth', 'make this do something')
  3. void doSomething() {
  4. print('do something');
  5. }