深入理解Flutter空安全

自 Dart 2.0 替换了静态可选类型系统为 健全的静态类型系统 后,空安全是我们对 Dart 作出最大的改变。在 Dart 初始之际,编译时的空安全是一项少有且需要大量时间推进的功能。让我们来看下面这个例子:

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

main() {
  isEmpty(null);
}

如果你在运行这个 Dart 程序时并未使用空安全,它将在调用 .length 时抛出 NoSuchMethodError 异常。 null 值是 Null 类的一个实例,而 Null 没有 “length” getter。运行时才出现的错误十分恼人,在本就是为终端打造的 Dart 语言上尤其如此。如果一个服务端应用出现了异常,你可以快速对它进行重启,而不被用户察觉。但当一个 Flutter 应用在用户的手机上崩溃了,他们的体验就会大打折扣。用户不开心,想必开发者也不会开心。

设计原则

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

  • 代码在默认情况下是安全的,除非开发者明确告知 Dart 变量可以为 null,否则它将认为该变量不可为空。选择这个作为默认选项,因为我们发现 non-nullable 是迄今为止 API 中最常见的选择。
  • 空安全的代码应可以轻松编写,因为还有有很多 Dart 代码需要修改,必须把它们逐步迁移到 null safety。在同一项目中应该可以包含 null safety 代码和 non-null-safe 代码,另外我们还将提供工具来帮助开发者进行迁移。
  • 产出的空安全代码应该是非常健全的,如上所述 Dart 的 null safety 是可靠的,将整个项目和依赖项迁移到null 安全之后,将获得稳健性带来的全部好处

声明变量的 null safety

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

// 都不可为空
var i = 42;
final b = Foo();
String m = '';

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

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

// 以下是可为空的
int? j = 1;  // 后续j可以为空
final Foo? c = getFoo();  // getFoo函数可以返回null.
String? n;  // 初始化和后续赋值都可以为null

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

// 函数的入参
void boogie(int? count) {
}
// 函数返回值
Foo? getFoo() {
}

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

使 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 之后,它也将是一项可选功能,开发者可以在准备就绪时采用它,你现有的代码将继续运行而无需更改


关于作者

dmd1990
获得点赞
文章被阅读