现代语言的空安全
所谓空安全,也就是null safety,它是部分现代语言具有的新特性,如dart和kotlin,既然它是现代语言才具备的特性,说明之前的语言往往没有,例如java
java的空值不安全
众所周知,java只有原始类型和引用类型,而所有引用类型都是Object类的子类或Object类它自己,而java的空值不安全,也就出现在引用类型上。
java的引用类型本质上就是指针,而作为深受C++影响的语言,它的引用类型也继承了C++的指针的问题——空指针问题
在java中定义的任何一个类,都是Obecjt的子类,任何一个类的对象,都可以为空,也就是为null,也就是说,引用类型包含了null!
所谓‘包含’,这又涉及一个概念,可以把一个类型看作一个集合,而这个类型的任何一个值看作这个集合中的一个元素,例如:
int类型表示集合 $\Set{ x | x \in Z \land -2^{31} \le x \le 2^{31} - 1 } $ ,即所有int值的集合float类型表示的集合比较复杂,首先float是离散的,能表示的值范围有限而且精度有限,而且不是等间距的,还包括了IEEE定义的inf,-inf,以及NaNObject类型表示的集合更复杂,但可以任何是程序运行过程中可能创建的所有Obejct对象以及它的子类的对象
而null值可以作为任意引用类型的对象的值,这在数学上造成了一个漏洞,造成了以下问题
- null值属于什么类型?
因为null可以作为任意引用类型的对象的值,所以null是任何引用类型的一个实例或它的子类的实例,但很明显这样的值是不能存在的 - 在类中定义的方法和属性,在类的实例(对象)上不一定可用
java的Object对象有一个方法toString(),然而你不能在null上调用这种方法,否则会抛出异常,严格来说,在使用toString()前应该先判断这个对象是否为null
这样的漏洞都可以使用引用类型来解释,因为引用类型的所有值都代表对某个对象的引用,所以这个值可以为空,表示没有引用任何对象
C++的空值安全?
C++的空值安全比较复杂,分为指针和引用两种类型
指针很明显空值不安全,java的空值不安全本质上还是指针的空值不安全
引用最大的优点就是,不存在空引用,也就是说,它缓解了C++的空指针问题,在合适的场合使用引用代替指针,可以减少空指针出现的情况
python的空值安全
python相比java和JS,有一个很明显的特点:一切皆对象,也就是说,python中的所有值都是对象,也就是object类的实例,自然None也是object的实例
然而,None是NoneType类的直接对象,也就是说,任何NoneType类以外的对象,都不可能为None,这么看来,python的空值设计比较合理
graph TD
object --> NoneType --唯一实例--> None
object --> str
object --> int
object --> ...
但问题在于,python是动态类型语言,也就是说变量运行时类型可以随意改变,在使用type hint之前,python并不能从空值安全的设计中受益
如果使用type hint,并引入严格的静态类型检查,python就是空值安全的。
- 假设你需要使用一个字符串变量,并且它可能为
None,就应该写成如果你需要使用字符串的x : str | None = "...."strip方法:这样的写法并不会通过诸如mypy这样的静态类型检查器,需要使用’type guard’保证x.strip()x的类型为str而不是str | Noneassert x is not None x.strip() # 或 assert isinstance(x, str) x.strip() # 或 if isinstance(x, str): x.strip() - 假设你需要使用一个字符串,而且它不可能为空,应该写成尝试给它赋值
x : str = "...."None,可以运行但是无法通过静态类型检查x : str = None # mypy报错 # 或 x : str = "...." x = None # mypy报错
dart的类型安全
dart3.0引入了一个新特性:空值安全
dart也是一切皆对象,而dart只有两类类型,一个是Null类,一个是Object类。从语言的底层设计就能看出dart对空值的态度
- 如果一个变量不能为空
String x = "...."
// 不能为空的变量必须在声明时赋初值,如果是类的属性,则必须在构造函数中赋值- 如果可能为空
String? x;
// 可空的变量,默认初值为nullString?不是任何传统的类型,而是两种类型的组合,类似python的str | None,如果使用前文提及的集合的观点,这种类型属于String类型和Null类型的集合并集,只不过dart并没有提供使用class关键字定义这个类的语法能力
此外dart是静态类型的语言,还拥有强大的类型推断能力(type inference),又提供了null相关的语法糖,能够避免写下很多琐碎的类型断言(例如python的assert x is not None)
null相关语法糖使用了 !和?,并和dart其他特性配合,组合出非常多的使用方式,例如在级联操作符中使用,在pattern中使用等等,例如
- null-aware如果
String? notAString = null; print(notAString?.length);notAString为null,不报错,表达式返回null - null-aware短路如果
showGizmo(Thing? thing) { print(thing?.doohickey.gizmo); }thing为null,不会评估表达式后面的部分,直接返回,避免写下thing?.doohickey?.gizmo这样繁琐的表达式 - null-aware与下标运算符
receiver?[index]; - null-aware与级联运算符
receiver?..method(); - null assert一旦error的运行时类型为
String toString() { if (code == 200) return 'OK'; return 'ERROR $code ${error!.toUpperCase()}'; }null,就会出现转型错误(cast error),抛出运行时异常 - 与pattern配合
String? maybeString = 'nullable with base type String'; switch (maybeString) { case var s?: // 's' has type non-nullable String here. }List<String?> row = ['user', null]; switch (row) { case ['user', var name!]: // ... // 'name' is a non-nullable string here. }(int?, int?) position = (2, 3); var (x!, y!) = position;
dart的null safety有一个特点:一个变量,只要编译器确定了它不为空,那么它永远不可能为null,这种特性被称为sound null safety