阅读 5013

【译】Flutter 1.20 中悄然带来的 null safety

原文链接:https://medium.com/dartlang/announcing-sound-null-safety-defd2216a6f3

随着 Flutter 1.20 正式版的发布,Dart 2.9 中的 null safety 已经可以是试验性使用,所以本篇翻译将介绍 Dart 中的 null safety 是什么。

对于 Dart 团队而言今天是 null safety 技术预览的一个重要里程碑,null safety 可以帮助开发者避免一些日常开发中很难被发现的错误,并且额外的好处是可以改善性能,现在 null safety 的早期技术预览已经发布,期待大家的使用反馈。

这篇文章将介绍 Dart 小组推进 null safety 的计划,也从不同的途径解释了,为什么各种语言会需要我们所说的声明空安全。

为什么需要 null safety

Dart 是一种类型安全的语言,这意味着当开发者获取某种类型的变量时,编译器可以保证它是该类型,但是类型安全本身不能保证变量不是 null

Null errors 非常常见的问题,在 GitHub上 可以搜索到成千上万由于 null 导致 Dart 代码出现异常的问题,甚至有成千上万的 commits 试图解决这些问题。

例如你尝试查看是否可以在以下示例代码中发现 nullability 问题:

void printLengths(List<File> files) {
  for (var file in files) {
    print(file.lengthSync());
  }
}
复制代码

如果通过 null 调用此函数肯定会失败,但是还要考虑第二种情况:

void main() {
  // Error case 1: passing a null to files.
  printLengths(null);
  // Error case 2: passing list of files, containing a null item.
  printLengths([File('filename1'), File('filename2'), null]);
}
复制代码

而空安全功能可以使该问题消失了:

使用 null 安全,开发者就可以放心地对代码进行推导,不再有讨厌的运行时 null 引用错误。相反开发者可以在编写代码时就发现静态错误。

可靠的 null safety

Dart 的 null safety 是可靠的,这意味着 Dart 可以 100% 确保,在上面的示例中 files 列表及其中的元素不能为 null 。当 Dart 分析代码并确定某个变量不可为空时,该变量将始终不能为空:如果你在 debugger 模式检查正在运行的代码,你将会发现 non-nullability 会在运行时保留。

Dart 的 null safety 可靠还具有另一个受欢迎的作用:可以让程序变得可以更小,更快。因为 Dart 可以确定 files 永远不会 null,所以 Dart 可以优化执行。例如 Dart 提前(AOT)编译可以生成更小、更快的本机代码,因为当知道变量不会为空时,不需要添加对 null 的检查。

我们初步已经看到了一些非常有希望的结果,例如在 Flutter framework 模拟渲染模式的 microbenchmark 测试中看到了19%的性能提升

设计原则

在开始针对 null safety 的详细设计之前,Dart 团队定义了以下三个核心原则:

  • 默认情况下不可为空,除非开发者明确告知 Dart 变量可以为 null,否则它将认为该变量不可为空。选择这个作为默认选项,因为我们发现 non-nullable 是迄今为止 API 中最常见的选择。

  • 逐步采用,因为还有有很多 Dart 代码需要修改,必须把它们逐步迁移到 null safety。在同一项目中应该可以包含 null safety 代码和 non-null-safe 代码,另外我们还将提供工具来帮助开发者进行迁移。

  • 完全可靠,如上所述 Dart 的 null safety 是可靠的,将整个项目和依赖项迁移到null 安全之后,将获得稳健性带来的全部好处。

声明变量的 null safety

核心语法很简单,提供一些不同方式来声明 non-nullable 变量,并且默认值是不可为空的,因此这些声明看起来和之前好像一样,但是它们的含义发生了变化。

// In null-safe Dart, none of these can ever be null.
var i = 42;
final b = Foo();
String m = '';
复制代码

Dart 将确保绝不会分配给上述变量任何 null 值。如果尝试执行 i = null,则会出现静态分析错误和红色的弯曲提示,并且无法继续编译。

如果开发者希望变量可为空,如下所示则可以使用?

// These are all nullable variables.
int? j = 1;  // Can be null later.
final Foo? c = getFoo();  // Maybe the function returns null.
String? n;  // Is null at first. Can be null at any later time, too.
复制代码

另外也可以将 ? 在运用到其他需要的地方:

// In function parameters.
void boogie(int? count) {
  // It's possible that count is null.
}
// In function return values.
Foo? getFoo() {
  // Can return null instead of Foo.
}
// Also: generics, typedefs, type checks, etc.
// And any combination of the above.
复制代码

但是,我们是希望你可以不需要使用到 ? ,因为绝大多数类型都是可以不必为空的。

使 null safety 更简易使用

Dart 开发小组正在努力使 null safety 尽可能方便使用,例如下面的代码,该代码 if 用于检查空值:

void honk(int? loudness) {
  if (loudness == null) {
    // No loudness specified, notify the developer
    // with maximum loudness.
    _playSound('error.wav', volume: 11);
    return;
  }
  // Loudness is non-null, let's just clamp it to acceptable levels.
  _playSound('honk.wav', volume: loudness.clamp(0, 11));
}
复制代码

这里可以看到 Dart 已经足够智能,以至于在我们传递该 if 语句时,loudness 变量其实已经保证不会为 null。 因此 Dart 让我们调用该 clamp() 方法而无需进行判空。这种便利是通过 Flow analysis 的方法来实现:Dart分析器像执行代码一样浏览你的代码,自动找出有关代码的其他信息

这是还有另一个示例,它显示了 Dart 可以确保变量为非 null 的情况,因为我们总是为其分配一个非 null 值:

int sign(int x) {
  // The result is non-nullable.
  int result;
  if (x >= 0) {
    result = 1;
  } else {
    result = -1;
  }
  // By this point, Dart knows the result cannot be null.
  return result;
}
复制代码

如果开发者删除了上述部分逻辑(例如,通过删除 result = -1;),则 Dart 无法保证 result 为非 null,所以开发者将得到一个静态错误,并且代码会无法编译。

Flow analysis 仅在函数内部起作用,如果你有全局变量或类字段,则 Dart 无法保证何时为其分配什么值,Dart 也无法为整个应用程序流程建模。因此,当你在第一次读取变量之前,就知道变量将为非空时,可以使用 late 关键字声明,但是不能立即对其进行初始化。

class Goo {
  late Viscosity v;
  Goo(Material m) {
    v = m.computeViscosity();
  }
}
复制代码

请注意 v 它是非空的,尽管它开始时未初始化,但是 Dart 相信开发者不会在没有 v 赋值之前尝试读取它,所以代码可以正确编译。

null safety 是向后兼容

Dart 团队已经进行了一年多的工作,以确保 null safety 的技术预览可靠。自从我们引入 Dart 2 以来,这是 Dart 语言最大的功能添加,但这并不是一个重大变化。现有代码可以使用null safety 代码,反之亦然。即使在提供 null safety 之后,它也将是一项可选功能,开发者可以在准备就绪时采用它,你现有的代码将继续运行而无需更改

我们最近迁移了 Dart 的核心库,以完全使用 null safety。作为 null safety 是向后兼容的一个示例,我们替换了现有的核心库,而在 Dart 和 Flutter 测试环境中运行的现有测试和测试应用程序没有任何 break。

我们甚至将许多新的核心库直接投放给 Google 的内部客户,这些客户直接运行进入了他们的生产代码库。我们计划迁移所有软件包和应用程序,以在功能启动时使用 null safety ,希望你也能这样做。

null safety 路线图

我们计划分三步逐步推出无效安全性:

  • 技术预览 :可在 Dart 的 dev channel 中找到,因为此刻仍可能发生未知变化,因此暂时不要在生产代码中使用 null safety
  • Beta版 :在 Dart 的 beta channel 中将提供 null safety ,该功能将非常接近预期的最终版本。如果拥有 pub.dev 软件包或插件,则可以开始迁移,但现在还不应该将其发布为稳定版本。
  • 稳定发布 :每个开发者都可以正常使用 null safety,因此建议你将迁移的软件包和插件发布为稳定版本。

如果一切顺利,我们计划在年底之前发布 null safety 到 stable,从现在开始我们将添加工具来帮助您使用 null safety,包括:

  • 迁移工具可支持你自动执行迁移升级,将现有软件包和应用程序迁移到 null safety;
  • 增加 pub.dev 上的标签,因此你可以判断软件包是否支持 null safety;
  • pub outdated 命令的扩展,支持支持查找 null safety 依赖关系的最新版本;

现在就试试

现在尝试 null safety 最快的方法是通过 nullsafety.dartpad.dev,这是启用了空安全性的 DartPad 版本,打开 “Learn with Snippets” 下拉列表找到一系列学习例子,这些学习例子介绍了新的语法和 null safety 的基础知识。

另外我们也有文档和产品更多的发布计划:

我们很高兴为 Dart 带来 null safety,可靠的 null safety 是 Dart 的一项独特功能,可帮助开发者编写更少容易出错的代码并获得更好的性能,我们同时也希望大家可以在技术预览版中试用该功能,并通过问题跟踪器向我们提供反馈。

最后,个人的额外提醒,目前在根目录的 analysis_options.yaml 添加如下配置就可以开启 null safety,另外 Flutter 需要 dart sdk 2.9

analyzer:
 enable-experiment:
 - non-nullable
复制代码