升级到Dart 2.14
空安全
随着Flutter的不断迭代更新,Dart语言也在快速发展。其中最重要的变化,是Dart 2.12正式发布的空安全特性。空安全特性是个分水岭,导致Dart语言面貌发生了很大的变化,许多库甚至直接放弃了升级空安全,停止了维护。这里我们升级Dart编程指南课程,一起来看一下,什么是空安全,怎么使用空安全?
类型系统中的可空性
因为一切均建立于静态类型系统上,所以空安全也始于此处。你的 Dart 程序中包含了整个类型世界:基本类型(如 int 和 String)、集合类型(如 List)以及你所使用的自定义类型。在空安全推出之前,静态类型系统允许所有类型的表达式中的每一处都可以有 null。
从类型理论的角度来说,Null 类型被看作是所有类型的子类;

类型会定义一些操作对象,包括 getters、setters、方法和操作符,在表达式中使用。如果是 List 类型,你可以对其调用 .add() 或 []。如果是 int 类型,你可以对其调用 +。但是 null 并没有定义任何一个方法。所以当 null 传递至其他类型的表达式时,任何操作都有可能失败。这就是空引用的症结所在——所有错误都来源于尝试在 null 上查找一个不存在的方法或属性。
非空和可空类型
空安全通过修改了类型的层级结构,从根源上解决了这个问题。 Null 类型仍然存在,但它不再是所有类型的子类。现在的类型层级看起来是这样的:

既然 Null 已不再被看作所有类型的子类,那么除了特殊的 Null 类型允许传递 null 值,其他类型均不允许。我们已经将所有的类型设置为 默认不可空 的类型。如果你的变量是 String 类型,它必须包含 一个字符串。这样一来,我们就修复了所有的空引用错误。
// 使用空安全
makeCoffee(String coffee, [String? dairy]) {
if (dairy != null) {
print('$coffee with $dairy');
} else {
print('Black $coffee');
}
}
此处我们希望 dairy 参数能传入任意字符串,或者一个 null 值。为了表达我们的想法,我们在原有类型 String 的尾部加上 ? 使得 dairy 成为可空的类型。本质上,这和定义了一个原有类型加 Null 的 组合类型 没有什么区别。String? 表达的意思相当于是 String|Null 的缩写。

要谨慎使用允许空值的情况,避免如下的代码:
bad(String? maybeString) {
print(maybeString.length);
}
main() {
bad(null);
}
以上代码将毫无疑问地崩溃。
关于Dart新特性空安全更多详细深入的理解,请转到Dart官方文档《深入理解空安全》
需要注意的是,如果使用Dart 2.12及以上版本,不仅我们自己写的代码需要支持不兼容的空安全新特性,所有依赖的第三方库也必须要支持空安全特性。
实际使用
变量中的空安全
class A {
int a;
String s;
// late延迟初始化
late bool b;
// 加?修饰,label值可以为null
String? label;
A(this.a, this.s) {
if (a > 0)
b = true;
else
b = false;
}
}
函数中的空安全
// name为空安全,使用时无需判空
void greet(String name) {
print(name.length);
print("hello," + name);
}
// 参数可为空方法中的使用
// 命名参数中增加了required关键字
void func({required String? abc, double? d}) {}
// 返回值为非空类型,不能返回null
String getName() {
// return null;
return '';
}
// 要返回包含null类型,也需要加?
String? user() {
return null;
}
泛型中的空安全
// list1变量不为空,但是列表中元素可为null
List<String?> list1;
// list2变量和列表中元素都可为null
List<String?>? list2;
// list3变量和列表中元素都不能为null
List<String> list3;
其他
不能使用List默认的构造方法List()
// 使用filled方法替代
List<int> list = List.filled(19, 0);
// 静态代码分析不出来,主动给编译器一个提示
// 1.使用“!”告诉编译器,变量不为空,消除编译错误
// 2.使用局部变量
if (list != null) {
if (list![0] != null) {}
}
使用布局变量消除编译错误
A? a;
if (a != null) {
var lab = a.label;
if (lab != null) {
print(lab.length);
}
}
类型别名
Dart 2.13 版推出了一个新特性,类型别名。
typedef Integer = int;
void main() {
print(int == Integer); // true
}
常见的用法是给某类型指定一个更短或更具描述性的名称,以便代码更易于理解和维护。例如:
// 定义一个类型别名
typedef Json = Map<String, dynamic>;
class User {
final String name;
final int age;
User.fromJson(Json json) :
name = json['name'],
age = json['age'];
Json get json => {
'name': name,
'age': age,
};
}
工具
废弃stagehand、dartfmt和dart2native命令,统一使用dart命令。
之前我们介绍使用pub global run stagehand console-full命令生成Dart命令行项目,现在使用下面的命令替代:
# 创建一个命令行模版工程
dart create -t console-full my_app
cd my_app
# 运行项目
dart run bin/my_app.dart
可以使用dart -h查看其他支持的参数。
以前使用dart2native编译,现在可以使用dart compile命令将 Dart 程序编译到目标平台:
# 生成独立的可执行文件
dart compile exe bin/myapp.dart
# 生成 AOT 快照
dart compile aot-snapshot bin/myapp.dart
# 生成JIT快照
dart compile jit-snapshot bin/myapp.dart
# 生成内核快照(一种可移植的 二进制快照)
dart compile kernel bin/myapp.dart
# 生成JavaScript
dart compile js bin/myapp.dart