概述
为什么需要空安全?
当你选择使用空安全时,代码中的类型将默认是非空的,意味着除非你声明它们可空,它们的值都不能为空。有了空安全,原本处于你的 运行时
的空值引用错误将变为 编辑时
的分析错误。这样可以大大降低空指针问题。
比如以下代码,string参数可能会报NoSuchMethodError异常。
1 | // Without null safety: |
健全的空安全已在 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 | // Without null safety: |
确保正确性
无效的返回值
1 | // Without null safety: |
在空安全以前,Dart会隐式的返回一个null。
未初始化的变量
顶层变量和静态字段必须包含一个初始化方法。
由于它们能在程序里的任何位置被访问到,编译器无法保证它们在被使用前已被赋值。唯一保险的选项是要求其本身包含初始化表达式,以确保产生匹配的类型的值。
1 |
|
实例的字段也必须在声明时包含初始化方法,可以为常见初始化形式,也可以在实例的构造方法中进行初始化。
这类初始化非常常见。举个例子:
1 |
|
- 局部变量的灵活度最高。一个非空的变量
不一定需要
一个初始化方法。这里有个很好的例子:
1 |
|
此处遵循的规则是局部变量必须确保在使用前被赋值
。
可选参数必须具有默认值。
如果一个可选位置参数或可选命名参数没有传递内容,Dart 会自动使用默认值进行填充。在未指定默认值的情况下,默认的 默认值为 null,如此一来,非空类型的参数就要出事了。
与可空类型共舞
智能的非空判断方法
1 | // Using null safety: |
空值断言操作符
有时候操作可空变量的某个属性,当变量为空,则会抛出异常。代码是通不过lint检测的,这个时候需要使用空值断言操作符,当出现异常会抛出。
1 | // Using null safety: |
懒加载的变量
late关键字是“在运行时而非编译时对变量进行约束”。这就让 late 这个词语约等于 何时
执行对变量的强制约束。
1 | // Using null safety: |
当前场景里,字段并不一定已经被初始化,每次它被读取时,都会插入一个运行时的检查,以确保它已经被赋值。如果并未赋值,就会抛出一个异常。给一个变量加上 String 类型就是在说:“我的值绝对是字符串”,而加上 late 修饰符意味着:“每次运行都要检查检查是不是真的”。
延迟初始化
1 |
|
当你这么声明时,会让初始化 延迟 执行。实例的构造将会延迟到字段首次被访问时执行,而不是在实例构造时就初始化。换句话说,它让字段的初始化方式变得与顶层变量和静态字段完全一致。当初始化表达式比较消耗性能,并且有可能不需要时,这会变得非常有用。
延迟的终值
1 | // Using null safety: |
与普通的 final 字段不同,你不需要在声明或构造时就将其初始化。你可以稍后在运行中的某个地方加载它。但是你只能对其进行 一次 赋值,并且它在运行时会进行校验。如果你尝试对它进行多次赋值,比如 heat() 和 chill() 都调用,那么第二次的赋值会抛出异常。这是确定字段状态的好方法,它最终会被初始化,并且在初始化后是无法改变的。
换句话说,新的 late 修饰符与 Dart 的其他变量修饰符结合后,已经实现了 Kotlin 中的 lateinit 和 Swift 中的 lazy 的大量特性。如果你需要给局部变量加上一些延迟初始化,你也可以在局部变量上使用它。
必需的命名参数
1 | //这里的所有参数都必须通过命名来传递。参数 a 和 c 是可选的,可以省略。参数 b 和 d 是必需的,调用时必须传递。在这里请注意,是否必需和是否可空无关。 |
总结
- 类型默认是非空的,可以添加 ? 变为可空的。
- 可选参数必须是可空的或者包含默认值的。你可以使用 required 来构建一个非可选命名参数。非空的全局变量和静态字段必须在声明时被初始化。实例的非空字段必须在构造体开始执行前被初始化。
- 如果接收者为 null,那么在其避空运算符之后的链式方法调用都会被截断。我们引入了新的空判断级联操作符 (?..) 及索引操作符 (?[])。后缀空断言“重点”操作符 (!) 可以将可空的操作对象转换为对应的非空类型。
- 新的流程分析,让你更安全地将可空的局部变量和参数,转变为可用的非空类型。它同时还对类型提升、遗漏的返回、不可达的代码以及变量的初始化,有着更为智能的规则。
- late 修饰符以在运行时每次都进行检查的高昂代价,让你在一些原本无法使用的地方,能够使用非空类型和 final。它同时提供了对字段延迟初始化的支持。
- List 类现在不再允许包含未初始化的元素。