Flutter - Dart语法
重要的概念
在学习 Dart 语言时, 应该基于以下事实和概念:
- 任何保存在变量中的都是一个 对象 , 并且所有的对象都是对应一个 类 的实例。 无论是数字,函数和
null
都是对象。所有对象继承自 Object 类。 - 尽管 Dart 是强类型的,但是 Dart 可以推断类型,所以类型注释是可选的。 在上面的代码中,
number
被推断为int
类型。 如果要明确说明不需要任何类型, 需要使用特殊类型dynamic
。 - Dart 支持泛型,如
List <int>
(整数列表)或List <dynamic>
(任何类型的对象列表)。 - Dart 支持顶级函数(例如
main()
), 同样函数绑定在类或对象上(分别是 静态函数 和 实例函数 )。 以及支持函数内创建函数 ( 嵌套 或 局部函数 ) 。 - 类似地, Dart 支持顶级 变量 , 同样变量绑定在类或对象上(静态变量和实例变量)。 实例变量有时称为字段或属性。
- 与 Java 不同,Dart 没有关键字 “public” , “protected” 和 “private” 。 如果标识符以下划线(_)开头,则它相对于库是私有的。 有关更多信息,参考 库和可见性。
- 标识符 以字母或下划线(_)开头,后跟任意字母和数字组合。
- Dart 语法中包含 表达式( expressions )(有运行时值)和 语句( statements )(没有运行时值)。 例如,条件表达式
condition ? expr1 : expr2
的值可能是expr1
或expr2
。 将其与 if-else 语句 相比较,if-else 语句没有值。 一条语句通常包含一个或多个表达式,相反表达式不能直接包含语句。 - Dart 工具提示两种类型问题:警告_和_错误。 警告只是表明代码可能无法正常工作,但不会阻止程序的执行。 错误可能是编译时错误或者运行时错误。 编译时错误会阻止代码的执行; 运行时错误会导致代码在执行过程中引发 [异常](#exception)。
一个简单的 Dart 程序
// 定义一个函数
printInteger(int aNumber) {
print('The number is $aNumber.'); // 打印到控制台。
}
// 应用从这里开始执行。
main() {
var number = 42; // 声明并初始化一个变量。
printInteger(number); // 调用函数。
}
变量
创建一个变量并进行初始化:
var name = 'Bob';
dynamic name = 'Bob';
String name = 'Bob';
默认值
未初始化的变量默认值是 null。即使变量是数字 类型默认值也是 null,因为在 Dart 中一切都是对象,数字类型 也不例外。
int lineCount;
assert(lineCount == null);
Final 和 Const
使用过程中从来不会被修改的变量, 可以使用 final
或 const
, 而不是 var
或者其他类型, Final 变量的值只能被设置一次; Const 变量在编译时就已经固定 (Const 变量 是隐式 Final 的类型.) 最高级 final 变量或类变量在第一次使用时被初始化。
final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';
内建类型
Dart 语言支持以下内建类型:
- Number
- String
- Boolean
- List (也被称为 Array)
- Map
- Set
- Rune (用于在字符串中表示 Unicode 字符)
- Symbol
Number
以下是将字符串转换为数字的方法,反之亦然:
// 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);
String
字符串可以通过 ${
expression}
的方式内嵌表达式。 如果表达式是一个标识符,则 {} 可以省略。 在 Dart 中通过调用就对象的 toString()
方法来得到对象相应的字符串。
var s = 'string interpolation';
assert('Dart has $s, which is very handy.' ==
'Dart has string interpolation, ' +
'which is very handy.');
使用连续三个单引号或者三个双引号实现多行字符串对象的创建:
var s1 = '''
You can create
multi-line strings like this one.
''';
var s2 = """This is also a
multi-line string.""";
使用 r
前缀,可以创建 “原始 raw” 字符串:
var s = r"In a raw string, even \n isn't special.";
Boolean
// 检查空字符串。
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);
List
// 提示: Dart 推断 list 的类型为 List<int> 。 如果尝试将非整数对象添加到此 List 中, 则分析器或运行时会引发错误。 有关更多信息,请阅读 类型推断。
var list = [1, 2, 3];
Set
在 Dart 中 Set 是一个元素唯一且无需的集合。 Dart 为 Set 提供了 Set 字面量和 Set 类型。
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
要创建一个空集,使用前面带有类型参数的 {}
,或者将 {}
赋值给 Set
类型的变量:
var names = <String>{};
// Set<String> names = {}; // 这样也是可以的。
// var names = {}; // 这样会创建一个 Map ,而不是 Set 。
Map
通常来说, Map 是用来关联 keys 和 values 的对象。 keys 和 values 可以是任何类型的对象。在一个 Map 对象中一个 key 只能出现一次。 但是 value 可以出现多次。 Dart 中 Map 通过 Map 字面量 和 Map 类型来实现。
var gifts = {
// Key: Value
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
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';
函数
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
// 隐藏返回值和回参仍可运行
isNoble(atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
如果函数中只有一句表达式,可以使用简写语法:
// => expr 语法是 { return expr; } 的简写。 => 符号 有时也被称为 箭头 语法。
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
可选参数
可选参数可以是命名参数或者位置参数,但一个参数只能选择其中一种方式修饰。
普通可选参数
int test2(int x, [int y, int z]) {
if (y == null || z == null) {
return x;
}
return x + y + z;
}
// tmp = 1
var tmp = test2(1);
有默认参数可选参数
// 因为可选参数有默认值, 所有不用判断参数是否为空
int test3(int x, [int y = 3, int z = 100]) {
return x + y + z;
}
// a = 16,b = 112, c = 15
int a = test3(2, 10, 4);
int b = test3(2, 10);
具名(命名)参数
可以有默认参数
int test4(int x, {int y = 3, int z = 111}) {
return x + y + z;
}
// c = 15
int c = test4(2, z: 10);
命名可选参数
定义函数是,使用 {*param1*, *param2*, …}
来指定命名参数:
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold, bool hidden}) {
}
调用函数时,可以使用指定命名参数 *paramName*: *value*
。 例如:
enableFlags(bold: true, hidden: false);
此时 Scrollbar
是一个构造函数, 当 child
参数缺少时,分析器会提示错误。
const Scrollbar({Key key, @required Widget child})
位置可选参数
将参数放到 []
中来标记参数是可选的:
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
下面是不使用可选参数调用上面方法 的示例:
assert(say('Bob', 'Howdy') == 'Bob says Howdy');
下面是使用可选参数调用上面方法的示例:
assert(say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal');
默认参数值
在定义方法的时候,可以使用 =
来定义可选参数的默认值。 默认值只能是编译时常量。 如果没有提供默认值,则默认值为 null。
下面是设置可选参数默认值示例:
/// 设置 [bold] 和 [hidden] 标志 ...
void enableFlags({bool bold = false, bool hidden = false}) {...}
// bold 值为 true; hidden 值为 false.
enableFlags(bold: true);
运算符
赋值运算符
使用 ??=
运算符时,只有当被赋值的变量为 null 时才会赋值给它。
// 如果b为空时,将变量赋值给b,否则,b的值保持不变。
b ??= value;
级联运算符 (..)
级联运算符 (..
) 可以实现对同一个对像进行一系列的操作。 除了调用函数, 还可以访问同一对象上的字段属性。 这通常可以节省创建临时变量的步骤, 同时编写出更流畅的代码。
考虑一下代码:
querySelector('#confirm') // 获取对象。
..text = 'Confirm' // 调用成员变量。
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));
第一句调用函数 querySelector()
, 返回获取到的对象。 获取的对象依次执行级联运算符后面的代码, 代码执行后的返回值会被忽略。
上面的代码等价于:
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
级联运算符可以嵌套,例如:
final addressBook = (AddressBookBuilder()
..name = 'jenny'
..email = 'jenny@example.com'
..phone = (PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();
在返回对象的函数中谨慎使用级联操作符。 例如,下面的代码是错误的:
var sb = StringBuffer();
sb.write('foo')
..write('bar'); // Error: 'void' 没哟定义 'write' 函数。
sb.write()
函数调用返回 void, 不能在 void
对象上创建级联操作。
异常
throw
下面是关于抛出或者 引发 异常的示例:
throw FormatException('Expected at least 1 section');
也可以抛出任意的对象:
throw 'Out of llamas!';
因为抛出异常是一个表达式, 所以可以在 => 语句中使用,也可以在其他使用表达式的地方抛出异常:
void distanceTo(Point other) => throw UnimplementedError();
catch
捕获异常可以避免异常继续传递(除非重新抛出( rethrow )异常)。 可以通过捕获异常的机会来处理该异常:
try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}
通过指定多个 catch 语句,可以处理可能抛出多种类型异常的代码。 与抛出异常类型匹配的第一个 catch 语句处理异常。 如果 catch 语句未指定类型, 则该语句可以处理任何类型的抛出对象:
try {
breedMoreLlamas();
} on OutOfLlamasException {
// 一个特殊的异常
buyMoreLlamas();
} on Exception catch (e) {
// 其他任何异常
print('Unknown exception: $e');
} catch (e) {
// 没有指定的类型,处理所有异常
print('Something really unknown: $e');
}
类
构造函数
通过创建一个与其类同名的函数来声明构造函数 (另外,还可以附加一个额外的可选标识符,如 命名构造函数 中所述)。 下面通过最常见的构造函数形式, 即生成构造函数, 创建一个类的实例:
class Point {
num x, y;
Point(num x, num y) {
// 使用 `this` 关键字引用当前实例。
this.x = x;
this.y = y;
}
}
通常模式下,会将构造函数传入的参数的值赋值给对应的实例变量, Dart 自身的语法糖精简了这些代码:
class Point {
num x, y;
// 在构造函数体执行前,
// 语法糖已经设置了变量 x 和 y。
Point(this.x, this.y);
}
默认构造函数
在没有声明构造函数的情况下, Dart 会提供一个默认的构造函数。 默认构造函数没有参数并会调用父类的无参构造函数。
构造函数不被继承
子类不会继承父类的构造函数。 子类不声明构造函数,那么它就只有默认构造函数 (匿名,没有参数) 。
命名构造函数
使用命名构造函数可为一个类实现多个构造函数, 也可以使用命名构造函数来更清晰的表明函数意图:
class Point {
num x, y;
Point(this.x, this.y);
// 命名构造函数
Point.origin() {
x = 0;
y = 0;
}
}
切记,构造函数不能够被继承, 这意味着父类的命名构造函数不会被子类继承。 如果希望使用父类中定义的命名构造函数创建子类, 就必须在子类中实现该构造函数。
调用父类非默认构造函数
默认情况下,子类的构造函数会自动调用父类的默认构造函数(匿名,无参数)。
如果父类中没有匿名无参的构造函数, 则需要手工调用父类的其他构造函数。 在当前构造函数冒号 (:
) 之后,函数体之前,声明调用父类构造函数。
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Person does not have a default constructor;
// you must call super.fromJson(data).
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
// Prints:
// in Person
// in Employee
if (emp is Person) {
// Type check
emp.firstName = 'Bob';
}
(emp as Person).firstName = 'Bob';
}
// 打印:in Person in Employee
初始化列表
除了调用超类构造函数之外, 还可以在构造函数体执行之前初始化实例变量。 各参数的初始化用逗号分隔。
// 在构造函数体执行之前,
// 通过初始列表设置实例变量。
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
警告: 初始化程序的右侧无法访问 this
。
在开发期间, 可以使用 assert
来验证输入的初始化列表。
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}
工厂构造函数
当执行构造函数并不总是创建这个类的一个新实例时,则使用 factory
关键字。 例如,一个工厂构造函数可能会返回一个 cache 中的实例, 或者可能返回一个子类的实例。
以下示例演示了从缓存中返回对象的工厂构造函数:
class Logger {
final String name;
bool mute = false;
// 从命名的 _ 可以知,_cache 是私有属性。
static final Map<String, Logger> _cache =
<String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
提示: 工厂构造函数无法访问 this。
工厂构造函的调用方式与其他构造函数一样:
var logger = Logger('UI');
logger.log('Button clicked');
方法
Getter 和 Setter
Getter 和 Setter 是用于对象属性读和写的特殊方法。 回想之前的例子,每个实例变量都有一个隐式 Getter ,通常情况下还会有一个 Setter 。 使用 get
和 set
关键字实现 Getter 和 Setter ,能够为实例创建额外的属性。
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 方法可以是抽象的, 只定义接口不进行实现,而是留给其他类去实现。 抽象方法只存在于 抽象类 中。
定义一个抽象函数,使用分号 (;) 来代替函数体:
abstract class Doer {
// 定义实例变量和方法 ...
void doSomething(); // 定义一个抽象方法。
}
class EffectiveDoer extends Doer {
void doSomething() {
// 提供方法实现,所以这里的方法就不是抽象方法了...
}
}
调用抽象方法会导致运行时错误。
隐式接口
每个类都隐式的定义了一个接口,接口包含了该类所有的实例成员及其实现的接口。 如果要创建一个 A 类,A 要支持 B 类的 API ,但是不需要继承 B 的实现, 那么可以通过 A 实现 B 的接口。
一个类可以通过 implements
关键字来实现一个或者多个接口, 并实现每个接口要求的 API。 例如:
// 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()));
}
下面示例演示一个类如何实现多个接口:
class Point implements Comparable, Location {
}
扩展类(继承)
使用 extends
关键字来创建子类, 使用 super
关键字来引用父类:
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}
重写类成员
子类可以重写实例方法,getter 和 setter。 可以使用 @override
注解指出想要重写的成员:
class SmartTelevision extends Television {
@override
void turnOn() {
}
}
noSuchMethod()
当代码尝试使用不存在的方法或实例变量时, 通过重写 noSuchMethod()
方法,来实现检测和应对处理:
class A {
// 如果不重写 noSuchMethod,访问不存在的实例变量时会导致 NoSuchMethodError 错误。
@override
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
}
除非符合下面的任意一项条件, 否则没有实现的方法不能够被调用:
- receiver 具有
dynamic
的静态类型 。 - receiver 具有静态类型,用于定义为实现的方法 (可以是抽象的), 并且 receiver 的动态类型具有
noSuchMethod()
的实现, 该实现与Object
类中的实现不同。
枚举类型
枚举类型也称为 enumerations 或 enums , 是一种特殊的类,用于表示数量固定的常量值。
使用枚举
使用 enum
关键字定义一个枚举类型:
enum Color { red, green, blue }
枚举中的每个值都有一个 index
getter 方法, 该方法返回值所在枚举类型定义中的位置(从 0 开始)。 例如,第一个枚举值的索引是 0 , 第二个枚举值的索引是 1。
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
使用枚举的 values
常量, 获取所有枚举值列表( list )。
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
可以在 switch 语句 中使用枚举, 如果不处理所有枚举值,会收到警告:
var aColor = Color.blue;
switch (aColor) {
case Color.red:
print('Red as roses!');
break;
case Color.green:
print('Green as grass!');
break;
default: // 没有这个,会看到一个警告。
print(aColor); // 'Color.blue'
}
枚举类型具有以下限制:
- 枚举不能被子类化,混合或实现。
- 枚举不能被显式实例化。
为类添加功能: Mixin
Mixin 是复用类代码的一种途径, 复用的类可以在不同层级,之间可以不存在继承关系。
通过 with
后面跟一个或多个混入的名称,来使用 Mixin , 下面的示例演示了两个使用 Mixin 的类:
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
通过创建一个继承自 Object 且没有构造函数的类,来 实现 一个 Mixin 。 如果 Mixin 不希望作为常规类被使用,使用关键字 mixin
替换 class
。 例如:
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
指定只有某些类型可以使用的 Mixin - 比如, Mixin 可以调用 Mixin 自身没有定义的方法 - 使用 on
来指定可以使用 Mixin 的父类类型:
mixin MusicalPerformer on Musician {
// ···
}
类变量和方法
使用 static
关键字实现类范围的变量和方法。
静态变量
静态变量(类变量)对于类级别的状态是非常有用的:
class Queue {
static const initialCapacity = 16;
// ···
}
void main() {
assert(Queue.initialCapacity == 16);
}
静态变量只到它们被使用的时候才会初始化。
泛型
在 API 文档中你会发现基础数组类型 List 的实际类型是 List<E>
。 <…> 符号将 List 标记为 泛型 (或 参数化) 类型。 这种类型具有形式化的参数。 通常情况下,使用一个字母来代表类型参数, 例如 E, T, S, K, 和 V 等。
为什么使用泛型
在类型安全上通常需要泛型支持, 它的好处不仅仅是保证代码的正常运行:
- 正确指定泛型类型可以提高代码质量。
- 使用泛型可以减少重复的代码。
如果想让 List 仅仅支持字符串类型, 可以将其声明为 List<String>
(读作“字符串类型的 list ”)。 那么,当一个非字符串被赋值给了这个 list 时,开发工具就能够检测到这样的做法可能存在错误。 例如:
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 错误
另外一个使用泛型的原因是减少重复的代码。 泛型可以在多种类型之间定义同一个实现, 同时还可以继续使用检查模式和静态分析工具提供的代码分析功能。 例如,假设你创建了一个用于缓存对象的接口:
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);
}
// 后来,又发现需要一个相同功能的数字类型接口 … 这里你应该明白了。
泛型可以省去创建所有这些接口的麻烦。 通过创建一个带有泛型参数的接口,来代替上述接口:
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
在上面的代码中,T 是一个备用类型。 这是一个类型占位符,在开发者调用该接口的时候会指定具体类型。
使用集合字面量
List , Set 和 Map 字面量也是可以参数化的。 参数化字面量和之前的字面量定义类似, 对于 List 或 Set 只需要在声明语句前加 <*type*>
前缀, 对于 Map 只需要在声明语句前加 <*keyType*, *valueType*>
前缀, 下面是参数化字面量的示例:
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
'index.html': 'Homepage',
'robots.txt': 'Hints for web robots',
'humans.txt': 'We are people, not machines'
};
使用泛型类型的构造函数
在调用构造函数的时,在类名字后面使用尖括号(<...>
)来指定泛型类型。 例如:
var nameSet = Set<String>.from(names);
下面代码创建了一个 key 为 integer, value 为 View 的 map 对象:
var views = Map<int, View>();
运行时中的泛型集合
Dart 中泛型类型是 固化的,也就是说它们在运行时是携带着类型信息的。 例如, 在运行时检测集合的类型:
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
提示: 相反,Java中的泛型会被 擦除 ,也就是说在运行时泛型类型参数的信息是不存在的。 在Java中,可以测试对象是否为 List 类型, 但无法测试它是否为 List<String>
。
限制泛型类型
使用泛型类型的时候, 可以使用 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>();
使用泛型函数
最初,Dart 的泛型只能用于类。 新语法_泛型方法_,允许在方法和函数上使用类型参数:
T first<T>(List<T> ts) {
// Do some initial work or error checking, then...
T tmp = ts[0];
// Do some additional checking or processing...
return tmp;
}
这里的 first
(<T>
) 泛型可以在如下地方使用参数 T
:
- 函数的返回值类型 (
T
). - 参数的类型 (
List<T>
). - 局部变量的类型 (
T tmp
).
库和可见性
import
和 library
指令可以用来创建一个模块化的,可共享的代码库。 库不仅提供了 API ,而且对代码起到了封装的作用: 以下划线 (_) 开头的标识符仅在库内可见。 每个 Dart 应用程序都是一个库 ,虽然没有使用 library
指令。
库可以通过包来分发。有关 pub(集成在SDK中的包管理器)的信息,请参考 Pub Package 和 Asset Manager。
使用库
通过 import
指定一个库命名空间中的内如如何在另一个库中使用。 例如,Dart Web应用程序通常使用 dart:html 库,它们可以像这样导入:
import 'dart:html';
import
参数只需要一个指向库的 URI。 对于内置库,URI 拥有自己特殊的dart:
方案。 对于其他的库,使用系统文件路径或者 package:
方案 。 package:
方案指定由包管理器(如 pub 工具)提供的库。例如:
import 'package:test/test.dart';
提示: URI 代表统一资源标识符。 URL(统一资源定位符)是一种常见的URI。
指定库前缀
如果导入两个存在冲突标识符的库, 则可以为这两个库,或者其中一个指定前缀。 例如,如果 library1 和 library2 都有一个 Element 类, 那么可以通过下面的方式处理:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// 使用 lib1 中的 Element。
Element element1 = Element();
// 使用 lib2 中的 Element。
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;
延迟加载库
Deferred loading (也称之为 lazy loading) 可以让应用在需要的时候再加载库。 下面是一些使用延迟加载库的场景:
- 减少 APP 的启动时间。
- 执行 A/B 测试,例如 尝试各种算法的 不同实现。
- 加载很少使用的功能,例如可选的屏幕和对话框。
要延迟加载一个库,需要先使用 deferred as
来导入:
import 'package:greetings/hello.dart' deferred as hello;
当需要使用的时候,使用库标识符调用 loadLibrary()
函数来加载库:
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
在前面的代码,使用 await
关键字暂停代码执行一直到库加载完成。 关于 async
和 await
的更多信息请参考 异步支持。
在一个库上你可以多次调用 loadLibrary()
函数。但是该库只是载入一次。
使用延迟加载库的时候,请注意一下问题:
- 延迟加载库的常量在导入的时候是不可用的。 只有当库加载完毕的时候,库中常量才可以使用。
- 在导入文件的时候无法使用延迟库中的类型。 如果你需要使用类型,则考虑把接口类型移动到另外一个库中, 让两个库都分别导入这个接口库。
- Dart 隐含的把
loadLibrary()
函数导入到使用deferred as *的命名空间*
中。loadLibrary()
方法返回一个 Future。
指定库前缀
如果导入两个存在冲突标识符的库, 则可以为这两个库,或者其中一个指定前缀。 例如,如果 library1 和 library2 都有一个 Element 类, 那么可以通过下面的方式处理:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// 使用 lib1 中的 Element。
Element element1 = Element();
// 使用 lib2 中的 Element。
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;
异步支持
Dart 库中包含许多返回 Future 或 Stream 对象的函数. 这些函数在设置完耗时任务(例如 I/O 操作)后, 就立即返回了,不会等待耗任务完成。 使用 async
和 await
关键字实现异步编程。 可以让你像编写同步代码一样实现异步操作。
处理 Future
可以通过下面两种方式,获得 Future 执行完成的结果:
- 使用
async
和await
. - 使用 Future API,具体描述,参考 库概览.
使用 async
和 await
关键字的代码是异步的。 虽然看起来有点想同步代码。 例如,下面的代码使用 await
等待异步函数的执行结果。
await lookUpVersion();
要使用 await
, 代码必须在 异步函数(使用 async
标记的函数)中:
Future checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
提示: 虽然异步函数可能会执行耗时的操作, 但它不会等待这些操作。 相反,异步函数只有在遇到第一个 await 表达式(详情见)时才会执行。 也就是说,它返回一个 Future 对象, 仅在await表达式完成后才恢复执行。
使用 try
, catch
, 和 finally
来处理代码中使用 await
导致的错误。
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}
在一个异步函数中可以多次使用 await
。 例如,下面代码中等待了三次函数结果:
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
在 await *表达式*
中, *表达式*
的值通常是一个 Future 对象; 如果不是,这时表达式的值会被自动包装成一个 Future 对象。 Future 对象指明返回一个对象的承诺(promise)。 await *表达式*
执行的结果为这个返回的对象。 await 表达式会阻塞代码的执行,直到需要的对象返回为止。
如果在使用 await 导致编译时错误, 确认 await 是否在一个异步函数中。 例如,在应用的 main()
函数中使用 await
, main()
函数的函数体必须被标记为 async
:
Future main() async {
checkVersion();
print('In main: version is ${await lookUpVersion()}');
}
声明异步函数
函数体被 async
标示符标记的函数,即是一个_异步函数_。 将 async
关键字添加到函数使其返回Future。 例如,考虑下面的同步函数,它返回一个 String :
String lookUpVersion() => '1.0.0';
例如,将来的实现将非常耗时,将其更改为异步函数,返回值是 Future 。
Future<String> lookUpVersion() async => '1.0.0';
注意,函数体不需要使用Future API。 如有必要, Dart 会创建 Future 对象。
如果函数没有返回有效值, 需要设置其返回类型为 Future<void>
。
处理 Stream
当需要从 Stream 中获取数据值时, 可以通过一下两种方式:
- 使用
async
和 一个 异步循环 (await for
)。 - 使用 Stream API, 更多详情,参考 in the library tour。
提示: 在使用 await for
前,确保代码清晰, 并且确实希望等待所有流的结果。 例如,通常不应该使用 await for
的UI事件侦听器, 因为UI框架会发送无穷无尽的事件流。
一下是异步for循环的使用形式:
await for (varOrType identifier in expression) {
// Executes each time the stream emits a value.
}
上面 *表达式*
返回的值必须是 Stream 类型。 执行流程如下:
- 等待,直到流发出一个值。
- 执行 for 循环体,将变量设置为该发出的值
- 重复1和2,直到关闭流。
使用 break或者
return语句可以停止接收 stream 的数据,这样就跳出了 for 循环, 并且从 stream 上取消注册。 **如果在实现异步 for 循环时遇到编译时错误, 请检查确保
await for处于异步函数中。** 例如,要在应用程序的
main()函数中使用异步 fo r循环,
main()函数体必须标记为
async` :
Future main() async {
// ...
await for (var request in requestServer) {
handleRequest(request);
}
// ...
}
有关异步编程的更多信息,请参考 dart:async 部分。 同时也可参考文章 Dart Language Asynchrony Support: Phase 1 和 Dart Language Asynchrony Support: Phase 2, 以及 Dart language specification 。
生成器
当您需要延迟生成( lazily produce )一系列值时, 可以考虑使用_生成器函数_。 Dart 内置支持两种生成器函数:
- Synchronous 生成器: 返回一个 Iterable 对象。
- Asynchronous 生成器: 返回一个 Stream 对象。
通过在函数体标记 sync*
, 可以实现一个同步生成器函数。 使用 yield
语句来传递值:
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
通过在函数体标记 async*
, 可以实现一个异步生成器函数。 使用 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);
}
}
转载自:https://juejin.cn/post/6932012405702524941