空安全:常见问题解答
- 迁移代码的用户应该注意哪些运行时更改?
- 如果一个值仅在测试中为 null 怎么办?
- @required 如何与新的 required 关键字比较?
- 应该如何迁移本应为 final 但不是 final 的非空字段?
- 应该如何迁移 built_value 类?
- 应该如何迁移可以返回 null 的工厂构造函数?
- 应该如何迁移现在显示为不必要的 assert(x != null)?
- 应该如何迁移现在显示为不必要的运行时 null 检查?
- Iterable.firstWhere 方法不再接受 orElse: () => null。
- 如何处理具有 setter 的属性?
- 如何表明 Map 的返回值是非空的?
- 为什么我的 List/Map 上的泛型类型是可空的?
- 默认的 List 构造函数发生了什么变化?
- 我在迁移时使用 package:ffi 并遇到 Dart_CObject_kUnsupported 错误。发生了什么?
- 为什么迁移工具在我的代码中添加注释?
- 关于编译到 JavaScript 和空安全,我应该了解什么?
- 资源
此页面收集了我们根据迁移 Google 内部代码的经验听到的关于空安全的一些常见问题。
迁移代码的用户应该注意哪些运行时更改?
#迁移的大部分影响不会立即影响迁移代码的用户
- 用户的静态空安全检查首先在他们迁移代码时应用。
- 当所有代码都迁移并且健全模式开启时,才会发生完整的空安全检查。
需要注意的两个例外是
!
运算符在所有模式下都是运行时 null 检查,适用于所有用户。因此,在迁移时,请确保仅在 null 流向该位置是错误的情况下添加!
,即使调用代码尚未迁移。- 与
late
关键字关联的运行时检查在所有模式下都适用,适用于所有用户。仅当您确定字段始终在使用前初始化时,才将其标记为late
。
如果一个值仅在测试中为 null 怎么办?
#如果一个值仅在测试中为 null,则可以通过将其标记为非空并使测试传递非空值来改进代码。
@required 如何与新的 required 关键字比较?
#@required
注解标记必须传递的命名参数;如果未传递,分析器将报告提示。
在空安全中,具有非空类型的命名参数必须具有默认值或使用新的 required
关键字标记。否则,将其设为非空类型就没有意义,因为它在未传递时会默认为 null
。
当从旧代码调用空安全代码时,required
关键字的处理方式与 @required
注解完全相同:未能提供参数将导致分析器提示。
当从空安全代码调用空安全代码时,未能提供 required
参数是一个错误。
这对迁移意味着什么?如果在以前没有 @required
的地方添加 required
,请务必小心。任何未传递新要求的参数的调用者将不再编译。相反,您可以添加默认值或使参数类型可为空。
应该如何迁移本应为 final
但不是 final
的非空字段?
#一些计算可以移动到静态初始化器中。而不是
// Initialized without values
ListQueue _context;
Float32List _buffer;
dynamic _readObject;
Vec2D(Map<String, dynamic> object) {
_buffer = Float32List.fromList([0.0, 0.0]);
_readObject = object['container'];
_context = ListQueue<dynamic>();
}
您可以这样做
// Initialized with values
final ListQueue _context = ListQueue<dynamic>();
final Float32List _buffer = Float32List.fromList([0.0, 0.0]);
final dynamic _readObject;
Vec2D(Map<String, dynamic> object) : _readObject = object['container'];
但是,如果字段是通过在构造函数中进行计算来初始化的,那么它不能是 final 的。在空安全中,您会发现这也会使其更难成为非空;如果它初始化得太晚,那么在初始化之前它为 null,并且必须是可空的。幸运的是,您有选择
- 将构造函数转换为工厂构造函数,然后使其委托给直接初始化所有字段的实际构造函数。此类私有构造函数的常用名称只是下划线:
_
。然后,该字段可以是final
和非空的。这种重构可以在迁移到空安全之前完成。 - 或者,将字段标记为
late final
。这强制它只初始化一次。它必须在使用前初始化。
应该如何迁移 built_value
类?
#使用 @nullable
注解的 getter 应该改为具有可空类型;然后删除所有 @nullable
注解。例如
@nullable
int get count;
变为
int? get count; // Variable initialized with ?
未标记 @nullable
的 getter 不应具有可空类型,即使迁移工具建议这样做。根据需要添加 !
提示,然后重新运行分析。
应该如何迁移可以返回 null
的工厂构造函数?
#首选不返回 null 的工厂构造函数。我们已经看到一些代码本意是由于无效输入而抛出异常,但最终却返回了 null。
而不是
factory StreamReader(dynamic data) {
StreamReader reader;
if (data is ByteData) {
reader = BlockReader(data);
} else if (data is Map) {
reader = JSONBlockReader(data);
}
return reader;
}
这样做
factory StreamReader(dynamic data) {
if (data is ByteData) {
// Move the readIndex forward for the binary reader.
return BlockReader(data);
} else if (data is Map) {
return JSONBlockReader(data);
} else {
throw ArgumentError('Unexpected type for data');
}
}
如果工厂构造函数的意图确实是返回 null,那么您可以将其转换为静态方法,以便它被允许返回 null。
应该如何迁移现在显示为不必要的 assert(x != null)
?
#当一切都完全迁移后,assert 将变得不必要,但就目前而言,如果您确实想保留检查,则它是必需的。选项
- 决定 assert 实际上不是必需的,并将其删除。这是启用 assert 时的行为更改。
- 决定始终可以检查 assert,并将其转换为
ArgumentError.checkNotNull
。这是禁用 assert 时的行为更改。 - 完全保持行为不变:添加
// ignore: unnecessary_null_comparison
以绕过警告。
应该如何迁移现在显示为不必要的运行时 null 检查?
#如果您将 arg
设置为非空,则编译器会将显式运行时 null 检查标记为不必要的比较。
if (arg == null) throw ArgumentError(...)`
如果程序是混合版本的,则必须包含此检查。在一切都完全迁移并且代码切换到以健全的空安全运行时之前,arg
可能会设置为 null
。
保留行为的最简单方法是将检查更改为 ArgumentError.checkNotNull
。
这同样适用于某些运行时类型检查。如果 arg
具有静态类型 String
,则 if (arg is! String)
实际上是在检查 arg
是否为 null
。迁移到空安全可能看起来意味着 arg
永远不能为 null
,但在不健全的空安全中它可能是 null
。因此,为了保留行为,应保留 null 检查。
Iterable.firstWhere
方法不再接受 orElse: () => null
。
#导入 package:collection
并使用扩展方法 firstWhereOrNull
而不是 firstWhere
。
如何处理具有 setter 的属性?
#与上面的 late final
建议不同,这些属性不能标记为 final
。通常,可设置属性也没有初始值,因为它们预计会在稍后某个时间设置。
在这种情况下,您有两个选择
将其设置为初始值。通常,省略初始值是错误而不是故意的。
如果您确定属性需要在访问之前设置,请将其标记为
late
。警告:
late
关键字添加了运行时检查。如果任何用户在set
之前调用get
,他们将在运行时收到错误。
如何表明 Map 的返回值是非空的?
#Map 上的查找运算符 ([]
) 默认返回可空类型。无法向语言发出信号表明该值保证存在。
在这种情况下,您应该使用 bang 运算符 (!
) 将值强制转换回 V
return blockTypes[key]!;
如果 map 返回 null,则会抛出异常。如果您想要显式处理这种情况
var result = blockTypes[key];
if (result != null) return result;
// Handle the null case here, e.g. throw with explanation.
为什么我的 List/Map 上的泛型类型是可空的?
#最终得到这样的可空代码通常是一种代码异味
List<Foo?> fooList; // fooList can contain null values
这意味着 fooList
可能包含 null 值。如果您正在使用长度初始化列表并通过循环填充它,则可能会发生这种情况。
如果您只是使用相同的值初始化列表,则应改用 filled
构造函数。
_jellyCounts = List<int?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
_jellyCounts[i] = 0; // List initialized with the same value
}
_jellyCounts = List<int>.filled(jellyMax + 1, 0); // List initialized with filled constructor
如果您正在通过索引设置列表的元素,或者您正在用不同的值填充列表的每个元素,则应改用列表字面量语法来构建列表。
_jellyPoints = List<Vec2D?>(jellyMax + 1);
for (var i = 0; i <= jellyMax; i++) {
_jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D
}
_jellyPoints = [
for (var i = 0; i <= jellyMax; i++)
Vec2D() // Each list element is a distinct Vec2D
];
要生成固定长度的列表,请使用 List.generate
构造函数,并将 growable
参数设置为 false
_jellyPoints = List.generate(jellyMax, (_) => Vec2D(), growable: false);
默认的 List 构造函数发生了什么变化?
#您可能会遇到此错误
The default 'List' constructor isn't available when null safety is enabled. #default_list_constructor
默认列表构造函数用 null 填充列表,这是一个问题。
将其更改为 List.filled(length, default)
。
我在迁移时使用 package:ffi
并遇到 Dart_CObject_kUnsupported
错误。发生了什么?
#通过 ffi 发送的列表只能是 List<dynamic>
,而不是 List<Object>
或 List<Object?>
。如果您在迁移中没有显式更改列表类型,则类型仍然可能因启用空安全时发生的类型推断更改而发生更改。
解决方法是显式地将此类列表创建为 List<dynamic>
。
为什么迁移工具在我的代码中添加注释?
#当迁移工具看到在健全模式下运行时始终为 false 或 true 的条件时,会添加 /* == false */
或 /* == true */
注释。此类注释可能表明自动迁移不正确,需要人工干预。例如
if (registry.viewFactory(viewDescriptor.id) == null /* == false */)
在这些情况下,迁移工具无法区分防御性编码情况和真正需要 null 值的情况。因此,该工具会告诉您它所知道的(“看起来这个条件将始终为 false!”)并让您决定该怎么做。
关于编译到 JavaScript 和空安全,我应该了解什么?
#空安全带来了许多好处,例如减小代码大小和提高应用性能。当编译为 Flutter 和 AOT 等原生目标时,这些好处更加明显。之前在生产 Web 编译器上的工作已经引入了类似于空安全后来引入的优化。这可能会使生产 Web 应用的最终收益看起来不如其原生目标。
一些值得强调的注意事项
生产 JavaScript 编译器生成
!
null 断言。在比较添加 null 断言之前和之后编译器的输出时,您可能不会注意到它们。那是因为编译器已经在非空安全的程序中生成了 null 检查。无论空安全的健全性或优化级别如何,编译器都会生成这些 null 断言。实际上,使用
-O3
或--omit-implicit-checks
时,编译器不会删除!
。生产 JavaScript 编译器可能会删除不必要的 null 检查。发生这种情况是因为生产 Web 编译器在空安全之前进行的优化在知道值不为 null 时删除了这些检查。
默认情况下,编译器将生成参数子类型检查。这些运行时检查确保协变虚拟调用具有适当的参数。编译器使用
--omit-implicit-checks
选项跳过这些检查。如果代码包含无效类型,使用此选项可能会生成具有意外行为的应用。为避免任何意外,请继续为您的代码提供强大的测试覆盖率。特别是,编译器基于输入应符合类型声明的事实优化代码。如果代码提供无效类型的参数,则这些优化将是错误的,并且程序可能会行为异常。这在以前对于不一致的类型是正确的,现在对于具有健全空安全的不一致的可空性也是正确的。您可能会注意到,开发 JavaScript 编译器和 Dart VM 对 null 检查有特殊的错误消息,但为了保持应用程序的小巧,生产 JavaScript 编译器没有。
您可能会看到错误,指示在 null 上找不到
.toString
。这不是 bug。编译器一直以这种方式编码一些 null 检查。也就是说,编译器通过对接收者的属性进行无保护访问来紧凑地表示一些 null 检查。因此,它不是if (a == null) throw
,而是生成a.toString
。toString
方法在 JavaScript Object 中定义,并且是验证对象是否不为 null 的一种快速方法。如果 null 检查之后的第一个操作是在值为空时崩溃的操作,则编译器可以删除 null 检查并让该操作导致错误。
例如,Dart 表达式
print(a!.foo());
可以直接转换为jsP.print(a.foo$0());
这是因为如果
a
为 null,则调用a.foo$()
会崩溃。如果编译器内联foo
,它将保留 null 检查。因此,例如,如果foo
是int foo() => 1;
,则编译器可能会生成jsa.toString; P.print(1);
如果内联方法首先访问接收者上的字段,例如
int foo() => this.x + 1;
,则生产编译器可以删除冗余的a.toString
null 检查(作为非内联调用),并生成jsP.print(a.x + 1);
资源
#除非另有说明,否则本网站上的文档反映了 Dart 3.7.1。页面上次更新于 2024-11-17。 查看源代码 或 报告问题。