1964 年,空指针崩溃的源头,以及 Dart2.10 最新的空安全特性!

1,522 阅读7分钟

本文收录于微信公众号 「Meandni」,转载注明来源或开设白名单, 点击阅读原文

2009 年,快排算法的作者、1980 年图灵奖的得主 Tony Hoare 在伦敦的 QCon 大会上发表了一场主题为《Null References: The Billion Dollar Mistake》的演讲,这场本该积极活跃的分享会全程却充满了懊悔和歉意,因为他认为正是自己在 1964 年将 null 指针引入编程语言的原因而导致了后来很多无法估量的损失。

托尼·霍尔(Tony Hoare)(右),1960 年在莫斯科国立大学作为交换生
托尼·霍尔(Tony Hoare)(右),1960 年在莫斯科国立大学作为交换生

按 Hoare 自己的描述,当时将 null 指针引入的 ALGO(Hoare 发明的语言)的原因是非常容易实现,而 Null 后来却也造成了 innerable errors, vulnerabilities 和 system crash 等等异常,也间接导致了几十年数十亿美元的损失

“我把 Null 引用称为自己的十亿美元错误。它的发明是在 1965 年,那时我用一个面向对象语言 ( ALGOL W ) 设计了第一个全面的引用类型系统。我的目的是确保所有引用的使用都是绝对安全的,编译器会自动进行检查。但是我未能抵御住诱惑,加入了Null引用,仅仅是因为实现起来非常容易。”

据调查,Javascript 中最常出现的 10 异常中有 8 个是由 nullundefined 引起

Javascript 最常见的错误排名
Javascript 最常见的错误排名

因此,这也算是一个语言设计者的锅了,关于对空指针的看法,现在的工程师们也各执己见,有人认为 null 指针反倒应当成为了程序语言必不可少的一部分,认为大师做的没错,而 Hoare 的好朋友 Edsger Dijkstra 后来却把 null 比作通奸犯,认为对象里的每个值为 null 的属性都是被 null XX 的 XX,笔者在这里不予评论

同时,现代很多高级语言,如 Kotlin, Swift, Rust,也已经有意识的来规避空指针这个特性了,今天我要说的 Dart,也在今年初开始重构类型体系支持这种在编译阶段就能发现空指针错误的 null-safe(空安全) 特性,也就是说今后我们将不再会因为空指针错误而导致 Flutter 应用程序崩溃啦!

Dart 2.10 的空安全特性

在 Dart 2.10 以前,我们就常常会犯如下的错误:

// Without null safety:
bool isEmpty(String string) => string.length == 0;

main() {
  isEmpty(null);
}

此时,因为这里的 null 属于 Null 类型,其中也并没有 length 属性的 getter 方法,因此会抛出 NoSuchMethodError 异常,这对我们这种底层的客户端开发者就是致命的错误。

强类型的语言的优势就在于能够在编译器就能立即发现程序的 bug,然后我们随之解决,开张大吉,但空指针错误却不在他的检查范围内,因此,Dart 在 2.10 引入 null-safe 特性后,我们在 IDE 内的编译期间就能不放过任何一个可能发生空指针错误的地方了

空安全语法错误提示
空安全语法错误提示

Google 官方表示, Dart 语言 null-safe 特性目前已经在做第二阶段的测试了,下个月将该特性合并入 Flutter 内测,在此之前我们可以在 DartPad 中尝尝鲜。

另外,空安全特性的引入直接要求我们写出没有空指针的代码,因此 Dart 编译器和运行时可以省去优化内部的空检查,应用程序会就变得更轻量、速度也会更快。

因为 Dart 目前也在预览阶段,暂时也不能投入生产,因此如果你在测试阶段遇到技术问题,随时可以向官方提出反馈

使用方法

null-safe 引入的新操作符和关键字包括 !late,使用过 Kotlin,TypeScript 或 C# 的读者,应该已经已经很熟悉这个特性了,下面我就介绍一下 Dart 语言中的使用方法。

此时,默认情况下,代码中写的所有变量默认都不为空:

// 这些变量都不为空,直接初始化
var i = 42;
String name = getFileName();
final b = Foo();

如果希望某个变量可以为空,可以使用 ? 操作符声明一个可空变量,如下:

int? aNullableInt = null;

如果你确定变量在使用之前一定会被初始化,可以使用 late 关键词延迟该操作:

class IntProvider {
  late int aRealInt;
  
  IntProvider() {
    aRealInt = calculate();
  }
}

这里,Dart analyzer 就不会要求 aRealInt 变量马上给一个指定的值,我们可以称这种变量为 late 变量

使用可控变量时,必须使用 ??if 语句做判空处理:

int value = aNullableInt ?? 0;

int definitelyInt(int? aNullableInt) {
  if (aNullableInt == null) {
    return 0;
  }
  return aNullableInt; // Can't be null!
}

如果你能保证一个 可空变量在使用时一定不为空,使用 ! 关键词声明:

int? aNullableInt = 2;
int value = aNullableInt!; // `aNullableInt!` 确定为 int .

如果你想要改变可空变量的类型,除了可以使用 操作符外,也可以使用 as

return maybeNum() as int;

最后,如果引入空安全特性,也不能对可空变量直接使用 . 操作符调用方法,需要使用 ?.

double? d;  
print(d?.floor()); // 使用 .? 代替 .

时机合适后引入

因为引入null-safe 特性被声明的变量都将默认不为空,会影响到整个 Dart 类型系统的改动,所以,直接使用这一特性可能会对项目具有较大的侵略性,因此官方将 null-safe 暂时当做可选特性,建议我们找个恰当的时机再做这个比较冒险的操作。

逐步引入

官方也建议我们按照特定顺序来重构代码,先在独立模块的项目中引入该特性。如,如果 C 依赖于 B,而 B 又依赖于 A,那么先将 A 重构到空安全,然后再重构 B,再重构 C,无论 A、B、C 是库、包还是应用应当遵循这个重构顺序。

为什么按这个次序重构代码?,虽然我们可以直接重构项目代码,但如果依赖项在重构过程中改变了它们的 API,就有可能还要进行二次重构,官方也将提供一系列开发工具来帮助开发者找出哪些依赖库已经迁移,开源项目的作者注意了,为了避免发生意外错误,请等到你所有的依赖项都重构了之后再发布空安全版本。

自动化工具

依赖项都重构完成时,可以使用官方的迁移工具进行项目代码重构,开发工具通过分析所有现有代码,查找哪些声明可以为非空(保持不变),哪些声明必须为可空(使用可空标记 “?” 可空标记)。

迁移工具是界面交互式的,因此我们可以直接查看该工具推断出来的具有可空性的属性,如果不想重构检查结果中的代码,也可以使用 ? 符标记为可空

下一步

官方在会之后几个月中同步更多关于 Dart 空安全特性的进展和动态,届时也会请 Flutter 团队的开发者配合引入到框架中,关于空安全的具体用法可以参照 官方文档,如果想要更深入去理解,可以参考 Understanding null safety,里面详细介绍了 Dart 官方的设计动机和理念,我后期也会跟国内的读者做最新的同步,大家敬请期待。

延伸阅读

https://medium.com/dartlang/announcing-dart-2-10-350823952bd5

https://blog.maxkit.com.tw/2015/08/null-null-reference-is-billion-dollar.html?m=1

https://blog.csdn.net/turingbook/article/details/3954798

https://dev.to/joelnet/null-the-billion-dollar-mistake-maybe-just-nothing-1cak

欢迎大家和我交流,关注公众号「Meandni」及时阅读最新技术动态或加我的微信「MeandniBlog」。