目录

修复类型提升失败

类型提升( 类型提升 )发生在流分析能够可靠地确认具有 可空类型 的变量为 非空 ,并且从那时起不会改变的情况下。许多情况会削弱类型的健全性,导致类型提升失败。

此页面列出了类型提升失败的原因,以及如何修复这些问题的技巧。要了解有关流分析和类型提升的更多信息,请查看 理解空安全 页面。

字段提升不支持的语言版本

#

原因: 您正在尝试提升字段,但是字段提升是基于语言版本的,您的代码设置为 3.2 之前的语言版本。

如果您已经使用 SDK 版本 >= Dart 3.2,您的代码可能仍然明确针对较早的 语言版本 。这可能是因为:

  • 您的 pubspec.yaml 声明了 SDK 约束,其下限低于 3.2,或者
  • 您的文件顶部有 // @dart=version 注释,其中 version 低于 3.2。

示例:

baddart
// @dart=3.1

class C {
  final int? _i;
  C(this._i);

  void f() {
    if (_i != null) {
      int i = _i;  // 错误
    }
  }
}

消息:

'_i' 指的是一个字段。它无法被提升,因为字段提升仅在 Dart 3.2 及更高版本中可用。

解决方案:

确保您的库没有使用早于 3.2 的 语言版本 。检查文件顶部是否有过时的 // @dart=version 注释,或者检查您的 pubspec.yaml 中是否有过时的 SDK 约束下限

只有局部变量可以提升(Dart 3.2 之前)

#

原因: 您正在尝试提升属性,但在早于 3.2 的 Dart 版本中,只有局部变量可以提升,而您使用的版本早于 3.2。

示例:

baddart
class C {
  int? i;
  void f() {
    if (i == null) return;
    print(i.isEven);       // 错误
  }
}

消息:

'i' 指的是一个属性,因此无法提升。

解决方案:

如果您使用的是 Dart 3.1 或更早版本,请 升级到 3.2 或更高版本

如果您需要继续使用旧版本,请阅读 其他原因和解决方法

其他原因和解决方法

#

此页面上的其余示例记录了与版本不一致无关的提升失败原因,包括字段和局部变量失败的原因,并附带示例和解决方法。

通常,提升失败的常见修复方法包括以下一项或多项:

  • 将属性的值赋给具有所需不可空类型的局部变量。
  • 添加显式空检查(例如, i == null )。
  • 如果您确定表达式不可能为 null ,则使用 !as 作为 冗余检查

以下是如何创建局部变量(可以命名为 i )的示例,该变量保存 i 的值:

gooddart
class C {
  int? i;
  void f() {
    final i = this.i;
    if (i == null) return;
    print(i.isEven);
  }
}

此示例包含一个实例字段,但它也可以使用实例 getter、静态字段或 getter、顶级变量或 getter 或 this

以下是如何使用 i! 的示例:

gooddart
print(i!.isEven);

无法提升 this

#

原因: 您正在尝试提升 this ,但尚不支持 this 的类型提升。

一种常见的 this 提升场景是在编写 扩展方法 时。如果扩展方法的 on 类型 是可空类型,则需要进行空检查以查看 this 是否为 null

示例:

baddart
extension on int? {
  int get valueOrZero {
    return this == null ? 0 : this; // 错误
  }
}

消息:

无法提升 `this` 。

解决方案:

创建一个局部变量来保存 this 的值,然后执行空检查。

gooddart
extension on int? {
  int get valueOrZero {
    final self = this;
    return self == null ? 0 : self;
  }
}

只有私有字段可以提升

#

原因: 您正在尝试提升字段,但该字段不是私有的。

程序中的其他库可能会使用 getter 覆盖公共字段。因为 getter 可能不会返回稳定的值 ,并且编译器不知道其他库在做什么,所以无法提升非私有字段。

示例:

baddart
class Example {
  final int? value;
  Example(this.value);
}

void test(Example x) {
  if (x.value != null) {
    print(x.value + 1); // 错误
  }
}

消息:

'value' 指的是一个公共属性,因此无法提升。

解决方案:

将字段设为私有,可以让编译器确定没有外部库可以覆盖其值,因此可以安全地进行提升。

gooddart
class Example {
  final int? _value;
  Example(this._value);
}

void test(Example x) {
  if (x._value != null) {
    print(x._value + 1);
  }
}

只有 final 字段可以提升

#

原因: 您正在尝试提升字段,但该字段不是 final 的。

对于编译器来说,非 final 字段原则上可以在测试它们的时间和使用它们的时间之间随时修改。因此,编译器将非 final 可空类型提升为非可空类型是不安全的。

示例:

baddart
class Example {
  int? _mutablePrivateField;
  Example(this._mutablePrivateField);

  void f() {
    if (_mutablePrivateField != null) {
      int i = _mutablePrivateField; // 错误
    }
  }
}

消息:

'_mutablePrivateField' 指的是一个非 final 字段,因此无法提升。

解决方案:

将字段设为 final

gooddart
class Example {
  final int? _immutablePrivateField;
  Example(this._immutablePrivateField);

  void f() {
    if (_immutablePrivateField != null) {
      int i = _immutablePrivateField; // 正确
    }
  }
}

无法提升 Getter

#

原因: 您正在尝试提升 getter,但只有实例 字段 可以提升,实例 getter 不可以。

编译器无法保证 getter 每次都返回相同的结果。由于无法确认其稳定性,因此 getter 不安全提升。

示例:

baddart
import 'dart:math';

abstract class Example {
  int? get _value => Random().nextBool() ? 123 : null;
}

void f(Example x) {
  if (x._value != null) {
    print(x._value.isEven); // 错误
  }
}

消息:

'_value' 指的是一个 getter,因此无法提升。

解决方案:

将 getter 赋给局部变量:

gooddart
import 'dart:math';

abstract class Example {
  int? get _value => Random().nextBool() ? 123 : null;
}

void f(Example x) {
  final value = x._value;
  if (value != null) {
    print(value.isEven); // 正确
  }
}

无法提升外部字段

#

原因: 您正在尝试提升字段,但该字段标记为 external

外部字段不会提升,因为它们本质上是外部 getter;它们的实现是 Dart 之外的代码,因此编译器无法保证外部字段每次调用时都会返回相同的值。

示例:

baddart
class Example {
  external final int? _externalField;

  void f() {
    if (_externalField != null) {
      print(_externalField.isEven); // 错误
    }
  }
}

消息:

'_externalField' 指的是一个外部字段,因此无法提升。

解决方案:

将外部字段的值赋给局部变量:

gooddart
class Example {
  external final int? _externalField;

  void f() {
    final i = _externalField;
    if (i != null) {
      print(i.isEven); // 正确
    }
  }
}

与库中其他地方的 getter 冲突

#

原因: 您正在尝试提升字段,但同一库中的另一个类包含具有相同名称的具体 getter。

示例:

baddart
import 'dart:math';

class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? get _overridden => Random().nextBool() ? 1 : null;
}

void testParity(Example x) {
  if (x._overridden != null) {
    print(x._overridden.isEven); // 错误
  }
}

消息:

'_overriden' 无法提升,因为类 'Override' 中存在冲突的 getter。

解决方案 :

如果 getter 和字段相关并且需要共享它们的名称(例如,当其中一个覆盖另一个时,如上面的示例所示),则可以通过将值赋给局部变量来启用类型提升:

gooddart
import 'dart:math';

class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? get _overridden => Random().nextBool() ? 1 : null;
}

void testParity(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // 正确
  }
}

关于无关类的说明

#

请注意,在上例中,为什么提升字段 _overridden 是不安全的这一点很清楚:因为字段和 getter 之间存在覆盖关系。但是,即使类不相关,冲突的 getter 也会阻止字段提升。例如:

baddart
import 'dart:math';

class Example {
  final int? _i;
  Example(this._i);
}

class Unrelated {
  int? get _i => Random().nextBool() ? 1 : null;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // 错误
  }
}

另一个库可能包含一个类,该类将这两个不相关的类组合到同一个类层次结构中,这将导致函数 f 中对 x._i 的引用被分派到 Unrelated._i 。例如:

baddart
class Surprise extends Unrelated implements Example {}

void main() {
  f(Surprise());
}

解决方案:

如果字段和冲突的实体确实不相关,您可以通过为它们指定不同的名称来解决此问题:

gooddart
class Example {
  final int? _i;
  Example(this._i);
}

class Unrelated {
  int? get _j => Random().nextBool() ? 1 : null;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // 正确
  }
}

与库中其他地方的不可提升字段冲突

#

原因: 您正在尝试提升字段,但同一库中的另一个类包含具有相同名称的字段,该字段不可提升(出于此页面上列出的任何其他原因)。

示例:

baddart
class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? _overridden;
}

void f(Example x) {
  if (x._overridden != null) {
    print(x._overridden.isEven); // 错误
  }
}

此示例失败是因为在运行时, x 实际上可能是 Override 的实例,因此提升将是不安全的。

消息:

'overridden' 无法提升,因为类 'Override' 中存在冲突的不可提升字段。

解决方案:

如果字段实际上是相关的并且需要共享名称,那么您可以通过将值赋给 final 局部变量来启用类型提升:

gooddart
class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? _overridden;
}

void f(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // 正确
  }
}

如果字段不相关,则重命名其中一个字段,这样它们就不会冲突。阅读 关于无关类的说明

与隐式 noSuchMethod 转发器冲突

#

原因: 您正在尝试提升私有且最终的字段,但同一库中的另一个类包含 隐式 noSuchMethod 转发器 ,其名称与该字段相同。

这是不安全的,因为无法保证 noSuchMethod 会从一次调用到下一次调用返回稳定的值。

示例:

baddart
import 'package:mockito/mockito.dart';

class Example {
  final int? _i;
  Example(this._i);
}

class MockExample extends Mock implements Example {}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // 错误
  }
}

在此示例中, _i 无法提升,因为它可能解析为编译器在 MockExample 内生成的不可靠的隐式 noSuchMethod 转发器(也命名为 _i )。

编译器创建此隐式实现 _i 是因为 MockExample 承诺在其声明中实现 Example 时支持 _i 的 getter,但没有履行该承诺。因此,未定义的 getter 实现由 MocknoSuchMethod 定义 处理,该定义创建了相同名称的隐式 noSuchMethod 转发器。

此错误也可能发生在 不相关的类 中的字段之间。

消息:

'_i' 无法提升,因为类 'MockExample' 中存在冲突的 noSuchMethod 转发器。

解决方案:

定义相关的 getter,以便 noSuchMethod 不必隐式处理其实现:

gooddart
import 'package:mockito/mockito.dart';

class Example {
  final int? _i;
  Example(this._i);
}

class MockExample extends Mock implements Example {
  @override
  late final int? _i;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // 正确
  }
}

getter 被声明为 late 以与模拟的常规使用方法保持一致;在不涉及模拟的场景中,不必将 getter 声明为 late 即可解决此类型提升失败问题。

可能在提升后写入

#

原因: 您正在尝试提升可能自提升后已被写入的变量。

示例:

baddart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;           // (1)
  }
  if (!b) {
    print(i.isEven); // (2) 错误
  }
}

解决方案 :

在此示例中,当流分析到达 (1) 时,它会将 i 从不可空的 int 降级回可空的 int? 。人类可以看出 (2) 处的访问是安全的,因为没有包含 (1) 和 (2) 的代码路径,但是流分析不够聪明,无法看到这一点,因为它不跟踪单独 if 语句中条件之间的相关性。

您可以通过组合这两个 if 语句来解决此问题:

gooddart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;
  } else {
    print(i.isEven);
  }
}

在像这样的直线控制流情况下(没有循环),流分析在决定是否降级时会考虑赋值的右侧。因此,修复此代码的另一种方法是将 j 的类型更改为 int

gooddart
void f(bool b, int? i, int j) {
  if (i == null) return;
  if (b) {
    i = j;
  }
  if (!b) {
    print(i.isEven);
  }
}

可能在之前的循环迭代中写入

#

原因: 您正在尝试提升可能在循环的先前迭代中已被写入的内容,因此提升无效。

示例:

baddart
void f(Link? p) {
  if (p != null) return;
  while (true) {    // (1)
    print(p.value); // (2) 错误
    var next = p.next;
    if (next == null) break;
    p = next;       // (3)
  }
}

当流分析到达 (1) 时,它会向前看并看到 (3) 处对 p 的写入。但因为它向前看,所以它还没有计算出赋值右侧的类型,因此它不知道是否可以安全地保留提升。为了安全起见,它使提升无效。

解决方案 :

您可以通过将空检查移到循环顶部来解决此问题:

gooddart
void f(Link? p) {
  while (p != null) {
    print(p.value);
    p = p.next;
  }
}

如果 case 块有标签,则这种情况也可能出现在 switch 语句中,因为您可以使用带标签的 switch 语句来构造循环:

baddart
void f(int i, int? j, int? k) {
  if (j == null) return;
  switch (i) {
    label:
    case 0:
      print(j.isEven); // 错误
      j = k;
      continue label;
  }
}

同样,您可以通过将空检查移到循环顶部来解决此问题:

gooddart
void f(int i, int? j, int? k) {
  switch (i) {
    label:
    case 0:
      if (j == null) return;
      print(j.isEven);
      j = k;
      continue label;
  }
}

在 try 块中可能写入后捕获

#

原因: 变量可能已在 try 块中被写入,并且执行现在在 catch 块中。

示例:

baddart
void f(int? i, int? j) {
  if (i == null) return;
  try {
    i = j;                 // (1)
    // ... 其他代码 ...
    if (i == null) return; // (2)
    // ... 其他代码 ...
  } catch (e) {
    print(i.isEven);       // (3) 错误
  }
}

在这种情况下,流分析认为 i.isEven (3) 不安全,因为它无法知道异常可能在 try 块中的什么时间发生,因此它保守地假设它可能发生在 (1) 和 (2) 之间,此时 i 可能是 null

类似的情况可能发生在 tryfinally 块之间,以及 catchfinally 块之间。由于实现方式的历史原因,这些 try / catch / finally 情况不会考虑赋值的右侧,类似于循环中发生的情况。

解决方案 :

要解决此问题,请确保 catch 块不依赖于对在 try 块中更改的变量状态的假设。请记住,异常可能在 try 块中的任何时间发生,可能在 inull 时发生。

最安全的解决方案是在 catch 块中添加空检查:

gooddart
try {
  // ···
} catch (e) {
  if (i != null) {
    print(i.isEven); // (3) 由于上面的空检查,(3) 正确。
  } else {
    // 处理 i 为 null 的情况。
  }
}

或者,如果您确定在 inull 时不会发生异常,只需使用 ! 运算符:

dart
try {
  // ···
} catch (e) {
  print(i!.isEven); // (3) 由于 `!` ,(3) 正确。
}

子类型不匹配

#

原因: 您正在尝试提升到不是变量当前提升类型(或在提升尝试时不是子类型)的子类型的类型。

示例:

baddart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    if (o is Pattern /* (2) */) {
      print(o.matchAsPrefix('foo')); // (3) 错误
    }
  }
}

在此示例中, o 在 (1) 处提升为 Comparable ,但在 (2) 处没有提升为 Pattern ,因为 Pattern 不是 Comparable 的子类型。(其基本原理是,如果它确实提升了,那么您将无法使用 Comparable 上的方法。)请注意,仅仅因为 Pattern 不是 Comparable 的子类型… 并不意味着 (3) 处的代码是死代码; o 可能有一个类型——例如 String ——同时实现了 ComparablePattern

解决方案 :

一种可能的解决方案是创建一个新的局部变量,以便将原始变量提升为 Comparable ,并将新变量提升为 Pattern

dart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    Object o2 = o;
    if (o2 is Pattern /* (2) */) {
      print(
          o2.matchAsPrefix('foo')); // (3) 正确;o2 已提升为 `Pattern` 。
    }
  }
}

但是,以后编辑代码的人可能会尝试将 Object o2 更改为 var o2 。此更改使 o2 的类型为 Comparable ,这又带来了对象无法提升为 Pattern 的问题。

冗余类型检查可能是更好的解决方案:

gooddart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    if (o is Pattern /* (2) */) {
      print((o as Pattern).matchAsPrefix('foo')); // (3) 正确
    }
  }
}

另一种有时有效的解决方案是当您可以使用更精确的类型时。如果第 3 行只关心字符串,那么您可以在类型检查中使用 String 。因为 StringComparable 的子类型,所以提升有效:

gooddart
void f(Object o) {
  if (o is Comparable /* (1) */) {
    if (o is String /* (2) */) {
      print(o.matchAsPrefix('foo')); // (3) 正确
    }
  }
}

局部函数捕获写入

#

原因: 变量已被局部函数或函数表达式捕获写入。

示例:

baddart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... 使用 foo ...
  if (i == null) return; // (1)
  // ... 其他代码 ...
  print(i.isEven);       // (2) 错误
}

流分析认为,一旦达到 foo 的定义,它就可能随时被调用,因此根本不再安全地提升 i 。与循环一样,这种降级会发生在赋值右侧的类型无关。

解决方案 :

有时可以重构逻辑,以便提升在写入捕获之前:

gooddart
void f(int? i, int? j) {
  if (i == null) return; // (1)
  // ... 其他代码 ...
  print(i.isEven); // (2) 正确
  var foo = () {
    i = j;
  };
  // ... 使用 foo ...
}

另一种选择是创建一个局部变量,这样它就不会被写入捕获:

gooddart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... 使用 foo ...
  var i2 = i;
  if (i2 == null) return; // (1)
  // ... 其他代码 ...
  print(i2.isEven); // (2) 正确,因为 `i2` 没有被写入捕获。
}

或者您可以进行冗余检查:

dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... 使用 foo ...
  if (i == null) return; // (1)
  // ... 其他代码 ...
  print(i!.isEven); // (2) 由于 `!` 检查,(2) 正确。
}

在当前闭包或函数表达式之外写入

#

原因: 变量在闭包或函数表达式之外被写入,并且类型提升位置在闭包或函数表达式内部。

示例:

baddart
void f(int? i, int? j) {
  if (i == null) return;
  var foo = () {
    print(i.isEven); // (1) 错误
  };
  i = j;             // (2)
}

流分析认为无法确定 foo 何时可能被调用,因此它可能在 (2) 处的赋值后被调用,因此提升可能不再有效。与循环一样,这种降级会发生在赋值右侧的类型无关。

解决方案 :

一种解决方案是创建一个局部变量:

gooddart
void f(int? i, int? j) {
  if (i == null) return;
  var i2 = i;
  var foo = () {
    print(i2.isEven); // (1) 正确,因为 `i2` 之后没有更改。
  };
  i = j; // (2)
}

示例:

一个特别糟糕的情况是这样的:

baddart
void f(int? i) {
  i ??= 0;
  var foo = () {
    print(i.isEven); // 错误
  };
}

在这种情况下,人类可以看到提升是安全的,因为对 i 的唯一写入使用非空值,并且发生在创建 foo 之前。但是 流分析没有那么聪明

解决方案 :

同样,一种解决方案是创建一个局部变量:

gooddart
void f(int? i) {
  var j = i ?? 0;
  var foo = () {
    print(j.isEven); // 正确
  };
}

此解决方案有效,因为由于其初始值 (i ?? 0), j 被推断为具有不可空类型 (int)。因为 j 具有不可空类型,无论它是否稍后被赋值, j 都永远不会具有非空值。

在当前闭包或函数表达式之外捕获写入

#

原因: 您尝试提升的变量在闭包或函数表达式之外被写入捕获,但是此变量的使用在尝试提升它的闭包或函数表达式内部。

示例:

baddart
void f(int? i, int? j) {
  var foo = () {
    if (i == null) return;
    print(i.isEven); // 错误
  };
  var bar = () {
    i = j;
  };
}

流分析认为无法判断 foobar 的执行顺序;实际上, bar 甚至可能在执行 foo 的过程中被执行(由于 foo 调用了调用 bar 的内容)。因此,在 foo 内部根本无法安全地提升 i

解决方案 :

最好的解决方案可能是创建一个局部变量:

gooddart
void f(int? i, int? j) {
  var foo = () {
    var i2 = i;
    if (i2 == null) return;
    print(i2.isEven); // 正确,因为 i2 对此闭包是局部的。
  };
  var bar = () {
    i = j;
  };
}