修复类型提升失败
类型提升发生在流分析可以健全地确认一个可空类型的变量为非空,并且从那时起它将不会改变时。许多情况会削弱类型的健全性,导致类型提升失败。
本页列出了类型提升失败发生的原因,并提供了修复它们的技巧。要了解更多关于流分析和类型提升的信息,请查看理解空安全页面。
字段提升不支持的语言版本
#原因:您正在尝试提升一个字段,但字段提升是受语言版本限制的,而您的代码设置的语言版本低于 3.2。
如果您已经在使用 Dart 3.2 或更高版本的 SDK,您的代码可能仍然明确地针对较早的语言版本。这可能发生在以下情况:
- 您的
pubspec.yaml
声明了一个 SDK 约束,其下限低于 3.2,或者 - 您在文件顶部有一个
// @dart=version
注释,其中version
低于 3.2。
示例
// @dart=3.1
class C {
final int? _i;
C(this._i);
void f() {
if (_i != null) {
int i = _i; // ERROR
}
}
}
消息
'_i' refers to a field. It couldn't be promoted because field promotion is only available in Dart 3.2 and above.
解决方案
确保您的库没有使用早于 3.2 的语言版本。检查文件顶部是否存在过时的 // @dart=version
注释,或检查 pubspec.yaml
中是否存在过时的 SDK 约束下限。
只有局部变量可以提升(Dart 3.2 之前)
#原因:您正在尝试提升一个属性,但在 Dart 3.2 之前的版本中只有局部变量可以提升,并且您正在使用 3.2 之前的版本。
示例
class C {
int? i;
void f() {
if (i == null) return;
print(i.isEven); // ERROR
}
}
消息
'i' refers to a property so it couldn't be promoted.
解决方案
如果您正在使用 Dart 3.1 或更早版本,请升级到 3.2 或更高版本。
如果您需要继续使用旧版本,请阅读其他原因和变通方法
其他原因和变通方法
#本页的其余示例说明了与版本不一致无关的提升失败原因,包括字段和局部变量的失败,并提供了示例和变通方法。
通常,提升失败的常见修复方法包括以下一种或多种:
- 将属性的值赋值给您需要的非空类型的局部变量。
- 添加显式的空检查(例如,
i == null
)。 - 如果您确定表达式不可能是
null
,请使用!
或as
作为冗余检查。
以下是创建局部变量(可以命名为 i
)来保存 i
值的一个示例:
class C {
int? i;
void f() {
final i = this.i;
if (i == null) return;
print(i.isEven);
}
}
此示例展示了一个实例字段,但它也可以使用实例 getter、静态字段或 getter、顶层变量或 getter,或者this
。
以下是使用 i!
的一个示例:
print(i!.isEven);
无法提升 this
#原因:您正在尝试提升 this
,但目前尚不支持对 this
的类型提升。
一个常见的 this
提升场景是在编写扩展方法时。如果扩展方法的on
类型是可空类型,您会希望进行空检查以查看 this
是否为 null
。
示例
extension on int? {
int get valueOrZero {
return this == null ? 0 : this; // ERROR
}
}
消息
`this` can't be promoted.
解决方案
创建一个局部变量来保存 this
的值,然后执行空检查。
extension on int? {
int get valueOrZero {
final self = this;
return self == null ? 0 : self;
}
}
只有私有字段可以提升
#原因:您正在尝试提升一个字段,但该字段不是私有的。
程序中的其他库可能会用 getter 覆盖公共字段。因为getter 可能不会返回一个稳定的值,并且编译器无法知道其他库正在做什么,所以非私有字段无法提升。
示例
class Example {
final int? value;
Example(this.value);
}
void test(Example x) {
if (x.value != null) {
print(x.value + 1); // ERROR
}
}
消息
'value' refers to a public property so it couldn't be promoted.
解决方案
将字段设为私有可以确保编译器确定没有外部库可以覆盖其值,因此可以安全地进行提升。
class Example {
final int? _value;
Example(this._value);
}
void test(Example x) {
if (x._value != null) {
print(x._value + 1);
}
}
只有 final 字段可以提升
#原因:您正在尝试提升一个字段,但该字段不是 final 的。
对于编译器来说,非 final 字段原则上可以在测试它们和使用它们之间的任何时间被修改。因此,编译器将非 final 可空类型提升为非空类型是不安全的。
示例
class Example {
int? _mutablePrivateField;
Example(this._mutablePrivateField);
void f() {
if (_mutablePrivateField != null) {
int i = _mutablePrivateField; // ERROR
}
}
}
消息
'_mutablePrivateField' refers to a non-final field so it couldn't be promoted.
解决方案
将字段设为 final
class Example {
final int? _immutablePrivateField;
Example(this._immutablePrivateField);
void f() {
if (_immutablePrivateField != null) {
int i = _immutablePrivateField; // OK
}
}
}
Getter 无法提升
#原因:您正在尝试提升一个 getter,但只有实例字段可以提升,实例 getter 不可以。
编译器无法保证 getter 每次都返回相同的结果。由于它们的稳定性无法确认,因此 getter 不安全,无法提升。
示例
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); // ERROR
}
}
消息
'_value' refers to a getter so it couldn't be promoted.
解决方案
将 getter 赋值给局部变量
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); // OK
}
}
外部字段无法提升
#原因:您正在尝试提升一个字段,但该字段被标记为 external
。
外部字段不会提升,因为它们本质上是外部 getter;它们的实现是来自 Dart 之外的代码,因此编译器无法保证外部字段每次调用时都会返回相同的值。
示例
class Example {
external final int? _externalField;
void f() {
if (_externalField != null) {
print(_externalField.isEven); // ERROR
}
}
}
消息
'_externalField' refers to an external field so it couldn't be promoted.
解决方案
将外部字段的值赋给局部变量
class Example {
external final int? _externalField;
void f() {
final i = _externalField;
if (i != null) {
print(i.isEven); // OK
}
}
}
与库中其他地方的 getter 冲突
#原因:您正在尝试提升一个字段,但同一库中的另一个类包含一个同名的具体 getter。
示例
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); // ERROR
}
}
消息
'_overriden' couldn't be promoted because there is a conflicting getter in class 'Override'.
解决方案:
如果 getter 和字段相关并且需要共享它们的名称(例如其中一个覆盖另一个,如上例所示),那么您可以通过将值赋给局部变量来启用类型提升。
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); // OK
}
}
关于不相关类的注意事项
#请注意,在上面的示例中,很清楚为什么提升字段 _overridden
是不安全的:因为字段和 getter 之间存在覆盖关系。但是,即使类不相关,冲突的 getter 也会阻止字段提升。例如:
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; // ERROR
}
}
另一个库可能包含一个将两个不相关类组合到相同类层次结构中的类,这将导致函数 f
中对 x._i
的引用被分派到 Unrelated._i
。例如:
class Surprise extends Unrelated implements Example {}
void main() {
f(Surprise());
}
解决方案
如果字段和冲突实体确实不相关,您可以通过给它们不同的名称来解决问题。
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; // OK
}
}
与库中其他地方不可提升的字段冲突
#原因:您正在尝试提升一个字段,但同一库中的另一个类包含一个同名但不可提升的字段(出于本页列出的任何其他原因)。
示例
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); // ERROR
}
}
此示例失败是因为在运行时,x
实际上可能是 Override
的实例,因此提升将不健全。
消息
'overridden' couldn't be promoted because there is a conflicting non-promotable field in class 'Override'.
解决方案
如果字段确实相关并且需要共享名称,那么您可以通过将值赋给一个 final 局部变量来启用类型提升。
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); // OK
}
}
如果字段不相关,则重命名其中一个字段,使它们不冲突。请阅读关于不相关类的注意事项。
与隐式 noSuchMethod
转发器冲突
#原因:您正在尝试提升一个私有且 final 的字段,但同一库中的另一个类包含一个与该字段同名的隐式 noSuchMethod
转发器。
这是不健全的,因为无法保证 noSuchMethod
在每次调用时都会返回一个稳定的值。
示例
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; // ERROR
}
}
在此示例中,_i
无法提升,因为它可能解析为编译器在 MockExample
内部生成的非健全的隐式 noSuchMethod
转发器(也名为 _i
)。
编译器创建 _i
的这个隐式实现,是因为 MockExample
在其声明中实现 Example
时承诺支持 _i
的 getter,但未能履行该承诺。因此,未定义的 getter 实现由 Mock
的 noSuchMethod
定义处理,这会创建一个同名的隐式 noSuchMethod
转发器。
此失败也可能发生在不相关类的字段之间。
消息
'_i' couldn't be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'.
解决方案
定义有问题的 getter,这样 noSuchMethod
就不必隐式处理其实现。
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; // OK
}
}
该 getter 被声明为 late
,以与 mock 的通常用法保持一致;在不涉及 mock 的场景中,解决此类型提升失败并不需要将 getter 声明为 late
。
可能在提升后被写入
#原因:您正在尝试提升一个自其被提升以来可能已被写入的变量。
示例
void f(bool b, int? i, int? j) {
if (i == null) return;
if (b) {
i = j; // (1)
}
if (!b) {
print(i.isEven); // (2) ERROR
}
}
解决方案:
在此示例中,当流分析到达 (1) 时,它将 i
从非空 int
降级回可空 int?
。人类可以判断 (2) 处的访问是安全的,因为没有代码路径同时包含 (1) 和 (2),但流分析不够智能,无法看到这一点,因为它不跟踪独立 if
语句中条件之间的关联。
您可以通过合并两个 if
语句来解决此问题。
void f(bool b, int? i, int? j) {
if (i == null) return;
if (b) {
i = j;
} else {
print(i.isEven);
}
}
在像这样的直线控制流情况(没有循环)中,流分析在决定是否降级时会考虑赋值的右侧。因此,修复此代码的另一种方法是将 j
的类型更改为 int
。
void f(bool b, int? i, int j) {
if (i == null) return;
if (b) {
i = j;
}
if (!b) {
print(i.isEven);
}
}
可能在之前的循环迭代中被写入
#原因:您正在尝试提升可能在循环的先前迭代中已被写入的内容,因此提升被失效。
示例
void f(Link? p) {
if (p != null) return;
while (true) { // (1)
print(p.value); // (2) ERROR
var next = p.next;
if (next == null) break;
p = next; // (3)
}
}
当流分析到达 (1) 时,它会向前查看并看到 (3) 处对 p
的写入。但因为它正在向前查看,它尚未确定赋值右侧的类型,因此它不知道保留提升是否安全。为了安全起见,它使提升失效。
解决方案:
您可以通过将空检查移到循环顶部来解决此问题。
void f(Link? p) {
while (p != null) {
print(p.value);
p = p.next;
}
}
如果 case
块带有标签,此情况也可能出现在 switch
语句中,因为您可以使用带标签的 switch
语句来构造循环。
void f(int i, int? j, int? k) {
if (j == null) return;
switch (i) {
label:
case 0:
print(j.isEven); // ERROR
j = k;
continue label;
}
}
同样,您可以通过将空检查移到循环顶部来解决此问题。
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 块中可能写入后进入 catch 块
#原因:变量可能已在 try
块中被写入,并且执行现在位于 catch
块中。
示例
void f(int? i, int? j) {
if (i == null) return;
try {
i = j; // (1)
// ... Additional code ...
if (i == null) return; // (2)
// ... Additional code ...
} catch (e) {
print(i.isEven); // (3) ERROR
}
}
在这种情况下,流分析不认为 i.isEven
(3) 是安全的,因为它无法知道异常可能发生在 try
块中的哪个时间点,因此它保守地假设异常可能发生在 (1) 和 (2) 之间,当时 i
可能为 null
。
类似情况可能发生在 try
和 finally
块之间,以及 catch
和 finally
块之间。由于实现方式的历史遗留问题,这些 try
/catch
/finally
情况不会考虑赋值的右侧,这与循环中发生的情况类似。
解决方案:
要解决此问题,请确保 catch
块不依赖于关于在 try
块内更改的变量状态的假设。请记住,异常可能在 try
块执行期间的任何时间发生,可能在 i
为 null
时。
最安全的解决方案是在 catch
块内部添加空检查。
try {
// ···
} catch (e) {
if (i != null) {
print(i.isEven); // (3) OK due to the null check in the line above.
} else {
// Handle the case where i is null.
}
}
或者,如果您确定当 i
为 null
时不会发生异常,只需使用 !
运算符。
try {
// ···
} catch (e) {
print(i!.isEven); // (3) OK because of the `!`.
}
子类型不匹配
#原因:您正在尝试提升到的类型不是变量当前提升类型的子类型(或者在尝试提升时不是子类型)。
示例
void f(Object o) {
if (o is Comparable /* (1) */ ) {
if (o is Pattern /* (2) */ ) {
print(o.matchAsPrefix('foo')); // (3) ERROR
}
}
}
在此示例中,o
在 (1) 处被提升为 Comparable
,但在 (2) 处未被提升为 Pattern
,因为 Pattern
不是 Comparable
的子类型。(理由是,如果它被提升,那么您将无法在 Comparable
上使用方法。)请注意,仅仅因为 Pattern
不是 Comparable
的子类型并不意味着 (3) 处的代码是死代码;o
可能具有一个同时实现 Comparable
和 Pattern
的类型——例如 String
。
解决方案:
一种可能的解决方案是创建一个新的局部变量,以便原始变量被提升为 Comparable
,而新变量被提升为 Pattern
。
void f(Object o) {
if (o is Comparable /* (1) */ ) {
Object o2 = o;
if (o2 is Pattern /* (2) */ ) {
print(
o2.matchAsPrefix('foo'),
); // (3) OK; o2 was promoted to `Pattern`.
}
}
}
然而,稍后编辑代码的人可能会试图将 Object o2
更改为 var o2
。这种更改会使 o2
获得 Comparable
类型,从而再次导致对象无法提升为 Pattern
的问题。
冗余类型检查可能是一个更好的解决方案。
void f(Object o) {
if (o is Comparable /* (1) */ ) {
if (o is Pattern /* (2) */ ) {
print((o as Pattern).matchAsPrefix('foo')); // (3) OK
}
}
}
另一种有时有效的解决方案是使用更精确的类型。如果第 3 行只关心字符串,那么您可以在类型检查中使用 String
。由于 String
是 Comparable
的子类型,因此提升成功。
void f(Object o) {
if (o is Comparable /* (1) */ ) {
if (o is String /* (2) */ ) {
print(o.matchAsPrefix('foo')); // (3) OK
}
}
}
被局部函数写入捕获
#原因:该变量已被局部函数或函数表达式写入捕获。
示例
void f(int? i, int? j) {
var foo = () {
i = j;
};
// ... Use foo ...
if (i == null) return; // (1)
// ... Additional code ...
print(i.isEven); // (2) ERROR
}
流分析推断,一旦到达 foo
的定义,它可能随时被调用,因此完全不再安全地提升 i
。与循环一样,这种降级无论赋值的右侧类型如何都会发生。
解决方案:
有时,可以重构逻辑,使提升发生在写入捕获之前。
void f(int? i, int? j) {
if (i == null) return; // (1)
// ... Additional code ...
print(i.isEven); // (2) OK
var foo = () {
i = j;
};
// ... Use foo ...
}
另一种选择是创建一个局部变量,这样它就不会被写入捕获。
void f(int? i, int? j) {
var foo = () {
i = j;
};
// ... Use foo ...
var i2 = i;
if (i2 == null) return; // (1)
// ... Additional code ...
print(i2.isEven); // (2) OK because `i2` isn't write captured.
}
或者您可以进行冗余检查。
void f(int? i, int? j) {
var foo = () {
i = j;
};
// ... Use foo ...
if (i == null) return; // (1)
// ... Additional code ...
print(i!.isEven); // (2) OK due to `!` check.
}
在当前闭包或函数表达式之外被写入
#原因:变量在闭包或函数表达式之外被写入,而类型提升的位置在闭包或函数表达式内部。
示例
void f(int? i, int? j) {
if (i == null) return;
var foo = () {
print(i.isEven); // (1) ERROR
};
i = j; // (2)
}
流分析推断无法确定 foo
何时可能被调用,因此它可能在 (2) 处的赋值之后被调用,从而提升可能不再有效。与循环一样,这种降级无论赋值的右侧类型如何都会发生。
解决方案:
一个解决方案是创建一个局部变量。
void f(int? i, int? j) {
if (i == null) return;
var i2 = i;
var foo = () {
print(i2.isEven); // (1) OK because `i2` isn't changed later.
};
i = j; // (2)
}
示例
一个特别棘手的例子如下:
void f(int? i) {
i ??= 0;
var foo = () {
print(i.isEven); // ERROR
};
}
在这种情况下,人类可以看到提升是安全的,因为对 i
的唯一写入使用了非空值,并且发生在 foo
创建之前。但是流分析并没有那么智能。
解决方案:
同样,一个解决方案是创建一个局部变量。
void f(int? i) {
var j = i ?? 0;
var foo = () {
print(j.isEven); // OK
};
}
此解决方案有效,因为 j
由于其初始值 (i ?? 0
) 而被推断为非空类型 (int
)。由于 j
具有非空类型,无论它是否在以后被赋值,j
永远不会是 null
。
在当前闭包或函数表达式之外被写入捕获
#原因:您尝试提升的变量在闭包或函数表达式之外被写入捕获,但变量的此用法位于尝试提升它的闭包或函数表达式内部。
示例
void f(int? i, int? j) {
var foo = () {
if (i == null) return;
print(i.isEven); // ERROR
};
var bar = () {
i = j;
};
}
流分析推断无法确定 foo
和 bar
可能以何种顺序执行;事实上,bar
甚至可能在执行 foo
的过程中(由于 foo
调用了某个调用 bar
的东西)被执行。因此,在 foo
内部提升 i
完全不安全。
解决方案:
最好的解决方案可能是创建一个局部变量。
void f(int? i, int? j) {
var foo = () {
var i2 = i;
if (i2 == null) return;
print(i2.isEven); // OK because i2 is local to this closure.
};
var bar = () {
i = j;
};
}