查看原文
其他

构建健全的空安全

Flutter 谷歌开发者 2021-08-05

作者 / Filip Hráček & Michael Thomsen

这是 Dart 团队的一个重要里程碑: 我们为大家带来了空安全功能的技术预览版。空安全可以帮助您避免一类通常难以发现的错误,并且还可以带来一系列的性能改进。欢迎大家体验日前发布的早期技术预览版本,我们期待您的反馈。


本文将为大家介绍 Dart 团队推出空安全的计划,并解释空安全的健全之处,以及与其他语言之间的差异。



为什么需要空安全?


Dart 是一种类型安全的语言,即当您拿到某个类型的变量时,编译器可以保证它属于该类型。但是类型安全本身并不能保证变量不为空 (null)。

空值错误是非常常见的。在 GitHub 上搜索一下就能找到成千上万个由 Dart 代码中的空值引发的错误,更有成千上万的开发者在试图修复这些问题


  • GitHub 上的空值错误
    https://github.com/search?l=Dart&q=%22called+on+null%22&type=Issues
  • GitHub 上针对空值错误提交的修复方案
    https://github.com/search?l=Dart&q=%22called+on+null%22&type=Commits

试试看,您能否在下面的示例代码中发现可空性问题:
void printLengths(List<File> files) { for (var file in files) { print(file.lengthSync()); }}

如果用空值进行调用,这个函数肯定会出错。但还有第二种情况需要考虑:
void main() { // 错误情况 1: 将一个空文件传递给方法的 files 参数。    printLengths(null); // 错误情况 2: 将文件列表传递给 files,但列表中包含空项目。 printLengths([File('filename1'), File('filename2'), null]);}

空安全功能则可以解决这个问题:

有了空安全,您就可以更放心地使用代码。不再需要等到运行时才遭遇空引用错误,在编写代码时您就能看到静态错误提醒。


健全的空安全


Dart 的空安全机制是健全的。这意味着 Dart 能 100% 确保上面例子中的文件列表 (files),以及其中的元素都不能为空。当 Dart 分析代码,并确定某个变量不可空的时候,这个变量就会一直不可空: 如果您在调试器中检查正在运行的代码,就会看到它在运行时保留了不可空性。相比之下,其他的一些空安全实现则不够健全,在很多情况下,它们仍然需要在运行时执行空检查。Dart 与 Swift 都拥有健全的空安全,而有些编程语言在这方面还仍待改进。

Dart 健全的空安全还有另一层妙处: 您的程序可以更小更快。因为 Dart 能够真正确定 files 永不为空,所以 Dart 可以做出优化。例如,Dart 的运行前 (ahead-of-time, AOT) 编译器可以生成更小更快的原生代码,因为当它知道一个变量不会为空的时候,就不再需要添加空检查了。

我们已经看到了一些值得期待的前期结果。例如,在模拟典型 Flutter 框架渲染模式的小规模测试中,性能提升 19%


  • 空安全性能提升 19%

    https://gist.github.com/a-siva/07e8a5bfa8548a3041d44d5d1c6f3a40



设计原则


在开始进行详细的空安全设计之前,Dart 团队确立了以下三个核心原则:

  • 默认不可空。除非您明确告诉 Dart 一个变量是可空的,否则它将一概认为所有变量都不可空。我们之所以这么做,是因为我们看到不可空是 API 中最常见的选择。
  • 可逐步采用。用 Dart 编写的代码数量庞大,因此开发者们需要能逐步迁移至空安全。在同一个项目中,应该允许同时存在空安全和非空安全的代码。我们也会提供一些工具来帮助大家完成迁移。
  • 彻底健全。如上所述,Dart 提供健全的空安全。一旦您将整个项目和依赖都迁移到空安全,您就能尽享健全空安全的优势。


声明空安全变量


核心语法很简单。下面给出了一些不可空变量,但通过不同的方式声明。请记住,不可空是默认的,所以这些声明虽然看起来和我们今天常见的形式一样,但其含义已经不同。

// 在空安全的 Dart 中,下列变量皆不可空。var i = 42;final b = Foo();String m = '';

Dart 会确保您永远无法将空值赋予上述任何一个变量,如果您试图在一千行代码之后输入 "i = null",就会触发静态分析错误并看到红色的波浪下划线,程序也将无法编译。


如果您希望变量是可空的,您可以使用 "?",比如:

// 下列变量可空。int? j = 1; // 之后可被赋予 null 值。final Foo? c = getFoo(); // 方法可以返回 null。String? n; // 初始即为 null,之后任何时间也可以为 null。

上面这几个变量的行为会与如今所有变量的行为一致。


您也可以在其他地方使用 "?" 语法:

// 在函数参数里使用。void boogie(int? count) { // count 可以为 null。}// 在函数返回值使用。Foo? getFoo() { // 可以返回 null 值。}// 也可以在泛型 (generics)、函数别名 (typedefs)、类型检查 (type checks) 等处使用。// 上述用法均可混合使用。

但我们要再强调一遍,我们的目标是让您几乎不再需要使用 "?",您的绝大多数类型都将是不可空的。



让空安全更加易用


Dart 团队正在尽可能地使空安全方便使用。比如下面这段代码,它使用 "if" 来检查空值:
void honk(int? loudness) { if (loudness == null) { // 在没有给出 loudness 值时直接用最大音量告知开发者。 _playSound('error.wav', volume: 11); return; }
// Loudness 不为空,直接将其调整 (clamp) 至可接受的范围。 _playSound('honk.wav', volume: loudness.clamp(0, 11));}

需要强调的是,Dart 对这段代码的处理方式相当 "机智",通过分析它知道在 if 语句通过之后,loudness 这个变量就不可能为空值,因此如代码的第 8 行所示,它可以让我们直接调用 loudness 的 clamp() 方法,而无需额外的操作。这种便利性是通过流程分析 (flow analysis) 来实现的: Dart 分析器会像执行代码一样检查您的代码,自动找出代码中附加的信息。


下面还有一个例子。Dart 可以确定一个变量非空,因为我们总是给它分配一个非空值:

int sign(int x) { // result 不可空。 int result;
if (x >= 0) { result = 1;     } else {        result = -1;     }          // 运行到这里 Dart 就知道 result 不会为 null。     return result; }

如果您删除了上例中任意赋值语句 (例如删除 result = -1; 这一行),Dart 将无法保证 result 非空: 您会看到一个静态错误,代码则无法编译。


流程分析只在函数内部有效。如果您有全局变量或类字段,那么 Dart 无法保证什么时候会分配什么值给它。Dart 无法对整个应用的流程进行建模。出于这个原因,您可以使用新的 "late" 关键字: 您在第一次读取某个变量之前就知道它不会为空,但您不能立刻初始化它。

class Goo { late Viscosity v;
Goo(Material m) { v = m.computeViscosity();    }}

请注意上例中的 v 是非空的,尽管它开始时没有被初始化。Dart 相信您在给 v 分配一个非空值之前不会尝试去读取它,因此您的代码在编译时不会报错。


空安全支持向下兼容


Dart 团队通过一年多的努力将空安全推进至技术预览版。这是自 Dart 2 推出以来,我们对 Dart 语言最大的扩充。但这个变化却没有任何破坏性。现有代码可以调用使用了空安全的代码,反之亦然。即使在空安全正式可用之后,它也将是可选功能,您可以在准备好之后再采用它。您现有的代码不需要任何修改就可以继续运行


我们最近将 Dart 核心库全部迁移至空安全。作为空安全向下兼容的展示,我们直接替换了既有的运行在 Dart 和 Flutter 测试环境中的测试和示例应用中的核心库,而且没有引发任何问题。我们甚至将新的核心库发给许多 Google 内部的开发者,直接用于他们的生产环境,也没有出现任何问题。我们计划在该功能正式推出时,将所有 package 和应用迁移到空安全。我们希望您也这么做,但您完全可以根据自身节奏,逐个对 package 和应用进行迁移。


  • Dart 核心库
    https://api.dart.cn/dev/2.9.0-13.0.dev/index.html


空安全发布时间表


我们计划逐步推出空安全功能,分为三步:

  • 技术预览版。本周发布的版本,您可以在 Dart 的 dev 渠道获取。请继续阅读至本文的 "即刻上手体验" 部分了解详情。我们依然会做出修改,所以暂时不要在生产环境中使用此版本的空安全。不过请务必上手进行体验,并和我们分享您的反馈
  • 测试版。空安全将在 Dart 的 beta 渠道可用,不再被标记为 "实验" 状态。这时空安全将非常接近最终版本。如果您手上有 pub.dev package 或插件,这时就可以着手进行迁移,但暂时不要以稳定版的形式进行发布。
  • 稳定版。在这个阶段,所有的用户都可以使用空安全,我们也会鼓励您以稳定版的形式将迁移完成的 package 和插件进行发布。您也需要迁移自己的正式版应用。


  • dev 渠道

    https://dart.cn/get-dart#release-channels

  • 提交反馈

    https://github.com/dart-lang/sdk/issues/new?title=Null%20safety%20feedback:%20[issue%20summary]&labels=NNBD&body=Describe%20the%20issue%20or%20potential%20improvement%20in%20detail%20here

  • 实验状态

    https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md

  • pub.dev

    https://pub.flutter-io.cn


如果一切顺利的话,我们计划在年底前发布空安全功能的稳定版本。在此之前,我们将推出一些工具帮助您的代码实现空安全,包括:

  • 一款迁移工具,帮助您在升级现有 package 和应用时,在很多步骤中实现自动化。

  • 在 pub.dev 中提供标识,这样您就能知道某个 package 是否支持空安全。

  • pub outdated 命令进行扩展,让您可以查找支持空安全的最新版本依赖。


  • pub outdated
    https://dart.cn/tools/pub/cmd/pub-outdated


即刻上手体验


想要试用空安全,最快的方法是访问 nullsafety.dartpad.cn,这是一个启用了空安全的 DartPad 版本。打开 Learn with Snippets 下拉菜单即可找到一系列的练习项目,里面包含新的语法介绍和空安全的基础知识。

您也可以在小型命令行应用中尝试空安全 (我们还没有迁移像 Flutter 这样的大型框架)。您可以先下载一份 dev 渠道的 Dart SDK,然后下载这个 Dart CLI 示例应用 (可以在 GitHub repo 里找到,对应的 zip 包在这里)。示例应用的 README 文件中说明了如何通过空安全实验标识运行应用。示例中的其他文件提供了启动配置,供您在 VS Code 和 Android Studio 中进行调试。


  • 下载 dev 渠道的 Dart SDK
    https://github.com/dart-lang/samples/blob/master/null_safety/calculate_lix/README.md#dart-preview-sdk-installation
  • Dart CLI 示例应用
    https://github.com/dart-lang/samples/tree/master/null_safety/calculate_lix
  • GitHub repo
    https://github.com/dart-lang/samples
  • Zip 文件
    https://github.com/dart-lang/samples/archive/master.zip
  • README 文件
    https://github.com/dart-lang/samples/blob/master/null_safety/calculate_lix/README.md


我们为大家准备了一些文档,之后会进一步进行扩充:

  • 空安全指引

    https://dart.cn/null-safety

  • 空安全核心库 API 文档

    https://api.dart.cn/dev/2.9.0-13.0.dev/index.html


很高兴能为 Dart 带来健全的空安全功能。这个 Dart 独有的功能有助于减少代码编写中的错误,让您获得更好的性能。我们希望您能在技术预览版中尝试这个功能,并通过问题追踪页面和我们分享您的反馈。祝大家编程愉快!


  • 分享反馈
    https://github.com/dart-lang/sdk/issues/new?title=Null%20safety%20feedback:%20[issue%20summary]&labels=NNBD&body=Describe%20the%20issue%20or%20potential%20improvement%20in%20detail%20here


推荐阅读






 点击屏末 | 阅读原文 | 访问 Flutter 开发者社区中文资源



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存