泛型
如果你看过数组类型的API文档,你会发现有个类型为List
为什么要使用泛型
泛型仅仅需要类型安全,但却会带来许多好处:
- 指定泛型会让编写的代码更好
- 使用泛型可以有效减少代码重复
如果你仅仅想要一个List里面包含字符串,你可以将其定义为 List
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error
另一个使用泛型的原因是可以减少代码重复。泛型使您可以在多种类型之间共享单个接口和实现,同时仍可以利用静态分析。举个例子,你创建了一个接口用于缓存对象:
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
这个时候你需要一个仅仅支持字符串的版本,所以你创建了另一个接口:
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
然后你又想要一个仅支持number类型的版本…..
泛型可以仅使用一个接口就解决你的问题:
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
这个代码中,T是一个传入类型,仅仅是给后面定义的类型起占位符的作用
使用集合字面量
List,set,map字面量可以参数化。参数化字面量就跟我们平常看到的那些一样,除了
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
'index.html': 'Homepage',
'robots.txt': 'Hints for web robots',
'humans.txt': 'We are people, not machines'
};
在构造函数中使用参数类型
想要在构造函数中指定一种或者多种类型,在类名后面使用 <…> ,举个例子:
var nameSet = Set<String>.from(names);
下面的代码创建了一个key是integer类型,value是View类型的map:
var views = Map<int, View>();
泛型集合已经它们包含的类型
Dart中的泛型类型在整个运行时中都会携带它们的类型,举个例子,你可以测试一种集合的类型:
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
注意: 如Java中的泛型是可擦除的,在运行时集合的类型将会被去除,所以在Java中,你只能测试一个对象是List的,而不能判断它是List
的
限制参数类型
在实现一个泛型类型时,你如果想要限制一下参数类型,你可以使用 extends
class Foo<T extends SomeBaseClass> {
// Implementation goes here...
String toString() => "Instance of 'Foo<$T>'";
}
class Extender extends SomeBaseClass {...}
使用SomeBaseClass或者它的子类作为泛型类型参数都是可以的
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
如果不指定泛型参数也是可以的
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'
如果指定的类型不是SomeBaseClass类型或者其子类都会报错
var foo = Foo<Object>();
//static analysis: error/warning
使用泛型方法
一开始泛型支持的限制仅仅在class层面,现在你可以将类型参数添加在方法或者函数之前,称之为泛型方法:
T first<T>(List<T> ts) {
// Do some initial work or error checking, then...
T tmp = ts[0];
// Do some additional checking or processing...
return tmp;
}
在first
- 返回值可以使用类型 T
- 传入的参数可以使用类型 T
- 内部参数可以使用类型T
类库和可见性
import 和 library可以方便帮助你构建模块以及分享代码。库中虽然没有提供专门进行可见性管理的API,但是你可以在标识符前以下划线 _ 开头,就可以使其仅在当前library内可见。每一个Dart程序都是一个library,即使它并没有使用library指令
使用 packages 可以将library分开
使用Libraries
使用 import 来在一个库内指定另一个库的命名空间
举个例子,Dart web应用通常会使用 dart:html 库,因此它们可以像下面这样导入:
import 'dart:html';
import的唯一参数就是导入的库的URI。一些内置的库,都是以dart: 开头的。而一些第三方的库,你可以使用文件系统的路径以package: 开头。package: 开头指定的库通常由包管理器提供如 pub tool。举个例子:
import 'package:test/test.dart';
注意: URI是统一资源标识符的标准,URL(统一资源定位符)是一种URI
指定库前缀
如果你导入两个库,这两个库的标识符冲突了,这时候你可以给其中一个库或者两个库都指定一个前缀。举个例子,如果库1和库2都有一个Element类,你可以像下面这样编写代码:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// Uses Element from lib1.
Element element1 = Element();
// Uses Element from lib2.
lib2.Element element2 = lib2.Element();
部分导入
如果你想导入库中的一部分,你可以选择性的导入这个库,举个例子:
// Import only foo.
import 'package:lib1/lib1.dart' show foo;
// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;
懒加载库
延迟加载(也叫做懒加载)允许一个web应用在某个库需要的时候再加载这个库,下面的是你可以使用的延迟加载的情况:
- 用于减少web应用初始化的时间
- 提供A/B测试
- 一些很少会使用到的功能
注意: 只有Dart2js支持延迟加载, Dart虚拟机并不支持
想要懒加载一个库,你必须在import之前使用 deferred as
import 'package:greetings/hello.dart' deferred as hello;
当你需要库执行loadLibrary()时,使用库的标识符
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
上面这段代码中,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去等待从一个异步函数中获取结果:
await lookUpVersion();
要使用await,则代码必须在一个async函数中,这个函数被用async标记:
Future checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
注意: 尽管异步函数中可能会执行耗时操作的函数,但是它不会等待耗时操作完成。作为替换,异步函数仅仅会在第一次遇到await表达式之前执行。它返回一个Future对象,这个Future对象仅在await表达式完成之后才会恢复执行
使用 try,catch和finally来处理在使用await时遇到的问题:
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}
你可以在一个async函数中多次使用await。举个例子,下面的代码在获取函数结果时等待了三次:
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
在await表达式中,表达式的值通常都是一个Future。如果它不是,它的值也会自动被包装到一个Future对象中。Future对象会约定返回一个对象,这个返回的对象就是await表达式的值。await表达式会让执行暂停直到返回值可用时
如果你在使用await时遇到编译错误,请确认await使用在async函数中了。举个例子,在main函数中使用await,那么你的main函数主体必须被标记为async:
Future main() async {
checkVersion();
print('In main: version is ${await lookUpVersion()}');
}
声明异步函数
函数体用async进行标识的函数称之为异步函数
往函数中添加async关键字会让这个函数返回Future对象。举个例子,观察下面的这个同步函数,它返回一个字符串:
String lookUpVersion() => '1.0.0';
如果你将其转为异步函数,举个例子,因为Future的实现需要消耗时间,所以它的返回值是一个Future:
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
一个异步循环通常包含下面的代码块:
await for (varOrType identifier in expression) {
// Executes each time the stream emits a value.
}
表达式的值只能为Stream类型。这样的好处有下面这些:
- 直到stream返回一个值之前等待
- 执行for循环的主体,并将变量的值设置为emit的值
- 重复1和2,直到stream关闭
生成器
当你需要lazy生成值序列,你可以考虑使用生成器函数。Dart提供了两种内建类型的生成器:
- 同步生成器: 返回一个 Iterable 对象
- 异步生成器: 返回一个 Stream 对象
要实现一个同步生成器函数,在函数体前使用 sync* 标识,使用yield代码块来传递数据:
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
要实现一个异步生成器函数,在函数体前使用 sync* 标识,使用yield代码来传递数据:
Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}
如果你的生成器是递归的,你可以使用 yield* 来提高它的性能
Iterable<int> naturalsDownFrom(int n) sync* {
if (n > 0) {
yield n;
yield* naturalsDownFrom(n - 1);
}
}
可调用类
你可以通过实现call()方法,来允许像调用函数一样调用Dart类的实例
在下面的这个例子中,WannabeFunction类将会定义一个call方法,这个方法接收三个字符串参数并把他们组合在一起,通过空格分割,用一个感叹号结尾
class WannabeFunction {
String call(String a, String b, String c) => '$a $b $c!';
}
var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');
main() => print(out);
Isolates
大多数的电脑,甚至现在的手机平台都会有多核的CPU。开发者们通常会在多个CPU中分享多个线程的内存状态。但是这样做非常容易导致错误,并且写出来的代码通常非常的复杂
不同于线程,所有的Dart代码运行在isolate内部。每一个isolate都有它自己的内存,并且确保别的isolate不能访问到当前isolate的内存状态
Typedefs
类型别名,通常我们称之为 typedef,因为它声明的时候使用关键字typedef,这是一种给类型取别名的简单方法。下面就是一个简单的例子使用类型别名来声明一个别名叫 IntList:
typedef IntList = List<int>;
IntList il = [1, 2, 3];
使用类型别名的时候可以使用泛型类型参数:
typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1; // Verbose.
ListMapper<String> m2; // Same thing but shorter and clearer.
在大多数时候,推荐使用内联函数类型去替代在函数上使用类型别名。但是函数上使用类型别名也是可以用的
typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
assert(sort is Compare<int>); // True!
}
注解
在你的代码上使用注解可以为你的代码添加更多的信息。注解以符号 @ 开头,跟在后面的是编译时常量的引用,比如 deprecated,或者别的常量构造函数
所有代码都支持的两种注解 @deprecated 和 @override 。下面是deprecated注解的使用例子
class Television {
/// _Deprecated: Use [turnOn] instead._
@deprecated
void activate() {
turnOn();
}
/// Turns the TV's power on.
void turnOn() {...}
}
你可以定义你自己注解。下面是一个定义 todo 注解的例子,它接收两个参数:
library todo;
class Todo {
final String who;
final String what;
const Todo(this.who, this.what);
}
下面是使用 todo 注解的例子:
import 'todo.dart';
@Todo('seth', 'make this do something')
void doSomething() {
print('do something');
}