空安全

Catalogue   

概述

为什么需要空安全?

当你选择使用空安全时,代码中的类型将默认是非空的,意味着除非你声明它们可空,它们的值都不能为空。有了空安全,原本处于你的 运行时 的空值引用错误将变为 编辑时 的分析错误。这样可以大大降低空指针问题。

比如以下代码,string参数可能会报NoSuchMethodError异常。

1
2
3
4
5
6
7
// Without null safety:
bool isEmpty(String string) => string.length == 0;

main() {
isEmpty(null);
}

健全的空安全已在 Dart 2.12 和 Flutter 2 中可用。

深入理解空安全

类型系统中的可空性

静态类型系统中,你的Dart程序包含了整个类型世界:基本类型(如 int 和 String)、集合类型(如 List)以及你和你所使用的依赖所定义的类和类型。在空安全推出之前,静态类型系统允许所有类型的表达式中的每一处都可以有 null。

从类型理论的角度来说,Null 类型被看作是所有类型的子类;

类型会定义一些操作对象,包括 getters、setters、方法和操作符,在表达式中使用。如果是 List 类型,你可以对其调用 .add() 或 []。如果是 int 类型,你可以对其调用 +。但是 null 值并没有它们定义的任何一个方法。所以当 null 传递至其他类型的表达式时,任何操作都有可能失败。这就是空引用的症结所在——所有错误都来源于尝试在 null 上查找一个不存在的方法或属性。

非空和可空类型

空安全通过修改了类型的层级结构,从根源上解决了这个问题。 Null 类型仍然存在,但它不再是所有类型的子类。现在的类型层级看起来是这样的:

既然 Null 已不再被看作所有类型的子类,那么除了特殊的 Null 类型允许传递 null 值,其他类型均不允许。我们已经将所有的类型设置为 默认不可空 的类型。如果你的变量是 String 类型,它必须包含 一个字符串。这样一来,我们就修复了所有的空引用错误。

使用可空类型

Dart在实现空安全之前,是存在隐式转换的。为了保持健全性,编译器为 requireStringNotObject() 的参数静默添加了 as String 强制转换。

1
2
3
4
5
6
7
8
9
10
// Without null safety:
requireStringNotObject(String definitelyString) {
print(definitelyString.length);
}

main() {
Object maybeString = 'it is';
requireStringNotObject(maybeString);
}

确保正确性

无效的返回值

1
2
3
4
5
// Without null safety:
String missingReturn() {
// No return.
}

在空安全以前,Dart会隐式的返回一个null。

未初始化的变量

  • 顶层变量和静态字段必须包含一个初始化方法。 由于它们能在程序里的任何位置被访问到,编译器无法保证它们在被使用前已被赋值。唯一保险的选项是要求其本身包含初始化表达式,以确保产生匹配的类型的值。
1
2
3
4
5
6
7
8
9


// Using null safety:
int topLevel = 0;

class SomeClass {
static int staticField = 0;
}

  • 实例的字段也必须在声明时包含初始化方法,可以为常见初始化形式,也可以在实例的构造方法中进行初始化。 这类初始化非常常见。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13


// Using null safety:
class SomeClass {
int atDeclaration = 0;
int initializingFormal;
int initializationList;

SomeClass(this.initializingFormal)
: initializationList = 0;
}


  • 局部变量的灵活度最高。一个非空的变量 不一定需要 一个初始化方法。这里有个很好的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16


// Using null safety:
int tracingFibonacci(int n) {
int result;
if (n < 2) {
result = n;
} else {
result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
}

print(result);
return result;
}


此处遵循的规则是局部变量必须确保在使用前被赋值

  • 可选参数必须具有默认值。 如果一个可选位置参数或可选命名参数没有传递内容,Dart 会自动使用默认值进行填充。在未指定默认值的情况下,默认的 默认值为 null,如此一来,非空类型的参数就要出事了。

与可空类型共舞

智能的非空判断方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Using null safety:
showGizmo(Thing? thing) {
//doohickey后面可以不用?符号。
//Dart从 C# 相同功能的设计中借鉴了一个聪明的处理方法。当你在链式方法调用中使用避空运算符时,如果接收器被判断为 null,那么 整个链式调用的剩余部分都会被截断并跳过。
print(thing?.doohickey.gizmo);
}


// Null-aware cascade:
receiver?..method();

// Null-aware index operator:
receiver?[index];


function?.call(arg1, arg2);


空值断言操作符

有时候操作可空变量的某个属性,当变量为空,则会抛出异常。代码是通不过lint检测的,这个时候需要使用空值断言操作符,当出现异常会抛出。

1
2
3
4
5
6
// Using null safety:
String toString() {
if (code == 200) return 'OK';
return 'ERROR $code ${error!.toUpperCase()}';//使用!转换成非空
}

懒加载的变量

late关键字是“在运行时而非编译时对变量进行约束”。这就让 late 这个词语约等于 何时 执行对变量的强制约束。

1
2
3
4
5
6
7
8
9
10
// Using null safety:
class Coffee {
late String _temperature;

void heat() { _temperature = 'hot'; }
void chill() { _temperature = 'iced'; }

String serve() => _temperature + ' coffee';
}

当前场景里,字段并不一定已经被初始化,每次它被读取时,都会插入一个运行时的检查,以确保它已经被赋值。如果并未赋值,就会抛出一个异常。给一个变量加上 String 类型就是在说:“我的值绝对是字符串”,而加上 late 修饰符意味着:“每次运行都要检查检查是不是真的”。

延迟初始化

1
2
3
4
5
6
7

// Using null safety:
class Weather {
late int _temperature = _readThermometer();
}


当你这么声明时,会让初始化 延迟 执行。实例的构造将会延迟到字段首次被访问时执行,而不是在实例构造时就初始化。换句话说,它让字段的初始化方式变得与顶层变量和静态字段完全一致。当初始化表达式比较消耗性能,并且有可能不需要时,这会变得非常有用。

延迟的终值

1
2
3
4
5
6
7
8
9
10
// Using null safety:
class Coffee {
late final String _temperature;

void heat() { _temperature = 'hot'; }
void chill() { _temperature = 'iced'; }

String serve() => _temperature + ' coffee';
}

与普通的 final 字段不同,你不需要在声明或构造时就将其初始化。你可以稍后在运行中的某个地方加载它。但是你只能对其进行 一次 赋值,并且它在运行时会进行校验。如果你尝试对它进行多次赋值,比如 heat() 和 chill() 都调用,那么第二次的赋值会抛出异常。这是确定字段状态的好方法,它最终会被初始化,并且在初始化后是无法改变的。

换句话说,新的 late 修饰符与 Dart 的其他变量修饰符结合后,已经实现了 Kotlin 中的 lateinit 和 Swift 中的 lazy 的大量特性。如果你需要给局部变量加上一些延迟初始化,你也可以在局部变量上使用它。

必需的命名参数

1
2
//这里的所有参数都必须通过命名来传递。参数 a 和 c 是可选的,可以省略。参数 b 和 d 是必需的,调用时必须传递。在这里请注意,是否必需和是否可空无关。
function({int? a, required int? b, int? c, required int? d}) {}

总结

  • 类型默认是非空的,可以添加 ? 变为可空的。
  • 可选参数必须是可空的或者包含默认值的。你可以使用 required 来构建一个非可选命名参数。非空的全局变量和静态字段必须在声明时被初始化。实例的非空字段必须在构造体开始执行前被初始化。
  • 如果接收者为 null,那么在其避空运算符之后的链式方法调用都会被截断。我们引入了新的空判断级联操作符 (?..) 及索引操作符 (?[])。后缀空断言“重点”操作符 (!) 可以将可空的操作对象转换为对应的非空类型。
  • 新的流程分析,让你更安全地将可空的局部变量和参数,转变为可用的非空类型。它同时还对类型提升、遗漏的返回、不可达的代码以及变量的初始化,有着更为智能的规则。
  • late 修饰符以在运行时每次都进行检查的高昂代价,让你在一些原本无法使用的地方,能够使用非空类型和 final。它同时提供了对字段延迟初始化的支持。
  • List 类现在不再允许包含未初始化的元素。

参考