title | description | short-title |
---|---|---|
空安全:常见问题 |
帮助你迁移到空安全的常见问题解答 |
空安全常见问题 |
This page collects some common questions we've heard about null safety based on the experience of migrating Google internal code.
此处涵盖了一些我们在迁移 Google 内部代码至 空安全 时遇到的常见问题。
Most of the effects of migration do not immediately affect users of migrated code:
在空安全迁移中的大部分影响,不会立刻出现在刚刚迁移完成的开发者身上:
-
Static null safety checks for users first apply when they migrate their code.
静态的空安全检查,会在开发者完成迁移后立刻生效。
-
Full null safety checks happen when all the code is migrated and sound mode is turned on.
完整的空安全检查,只会在所有代码都已迁移,并且启用了完整的空安全模式时生效。
Two exceptions to be aware of are:
但是有两项例外你需要注意:
-
The
!
operator is a runtime null check in all modes, for all users. So, when migrating, ensure that you only add!
where it's an error for anull
to flow to that location, even if the calling code has not migrated yet.对于任何模式而言,
!
操作符都是在运行时进行的空检查。 所以在进行迁移时,请确保你仅对null
可能由混合模式造成污染的代码位置添加!
, 就算发起调用的代码还未迁移至空安全,也应如此。 -
Runtime checks associated with the
late
keyword apply in all modes, for all users. Only mark a fieldlate
if you are sure it is always initialized before it is used.late
会在运行时检查。所以请你仅在确定它被使用前一定会被初始化的情况下使用late
。
If a value is only ever null
in tests, the code can be improved by marking it
non-nullable and making the tests pass non-null values.
如果在测试中某个值始终为 null
,可以通过将测试传值和测试需要的值改为非空,来改进你的测试。
The @required
annotation marks named arguments that must be passed; if not,
the analyzer reports a hint.
@required
注解会将参数标记为必须传递。如果未传,分析器会给出一个提示。
With null safety, a named argument with a non-nullable type must either have a
default or be marked with the new required
keyword. Otherwise, it wouldn't
make sense for it to be non-nullable, because it would default to null
when
not passed.
有了空安全,非空类型的命名参数要么需要默认值,要么需要使用 required
关键字修饰。
否则,它在未传递时默认会是 null
,就显得不合理了。
When null safe code is called from legacy code the required
keyword is treated
exactly like the @required
annotation: failure to supply the argument will
cause an analyzer hint.
在旧的代码中,required
关键词会被看作 @required
注解,
即参数未传递时会显示一个分析器提示。
When null safe code is called from null safe code, failing to supply a
required
argument is an error.
当你在空安全的代码中使用空安全代码时,如果 required
修饰的参数未传递,
会显示一个错误。
What does this mean for migration? Be careful if adding required
where there
was no @required
before. Any callers not passing the newly-required argument
will no longer compile. Instead, you could add a default or make the argument
type nullable.
那么它对于迁移来说意味着什么呢?
当你给以前没有使用 @required
注解的参数加上 required
时要十分小心。
任何没有传递新需要的参数的代码,都无法进行编译。
实际上,你可以加上默认值,或是将参数变为可空类型。
Some computations can be moved to the static initializer. Instead of:
一些赋值计算可以移动到静态的初始化中。 与其使用下面的方式:
{:.bad} {% prettify dart tag=pre+code %} // 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(); } {% endprettify %}
you can do:
你可以这样做:
{:.good} {% prettify dart tag=pre+code %} // Initialized with values final ListQueue _context = ListQueue(); final Float32List _buffer = Float32List.fromList([0.0, 0.0]); final dynamic _readObject;
Vec2D(Map<String, dynamic> object) : _readObject = object['container']; {% endprettify %}
However, if a field is initialized by doing computation in the constructor, then
it can't be final
. With null safety, you'll find this also makes it harder for
it to be non-nullable; if it's initialized too late, then it's null
until it's
initialized, and must be nullable. Fortunately, you have options:
然而,在构造函数中通过计算进行初始化的字段,是无法为 final
的。
在使用空安全的时候,你会发现想让它们的类型为非空,也不是一件容易的事。
如果初始化的时机不合适,那么直到初始化前,它都只能是可空的类型。
幸运的是,你还有其他选择:
-
Turn the constructor into a factory, then make it delegate to an actual constructor that initializes all the fields directly. A common name for such a private constructor is just an underscore:
_
. Then, the field can befinal
and non-nullable. This refactoring can be done before the migration to null safety.将构造转变为工厂方法,并将其委托给一个直接初始化所有字段的真正的构造函数。 在 Dart 中,这样的私有构造通常是一个下划线:
_
。 如此一来,字段就可以是final
且非空了。 在空安全迁移介入 之前,你就可以这样进行调整。 -
Or, mark the field
late final
. This enforces that it's initialized exactly once. It must be initialized before it can be read.或者,将字段标记为
late final
。这会使得字段只被初始化一次。 在它被读取之前必须被初始化。
Getters that were annotated @nullable
should instead have nullable types; then
remove all @nullable
annotations. For example:
使用了 @nullable
注解的 getter 应当直接转变为可空的类型,
然后移除所有的 @nullable
注解。例如:
@nullable
int get count;
becomes
变成
int? get count; // Variable initialized with ?
Getters that were not marked @nullable
should not have nullable types,
even if the migration tool suggests them. Add !
hints as needed then rerun the
analysis.
就算迁移工具建议,没有 使用 @nullable
注解的 getter 也不应该是可空的类型。
这时可以根据需要添加 !
操作符,并且重新进行分析。
Prefer factories that do not return null. We have seen code that meant to throw an exception due to invalid input but instead ended up returning null.
优先使用不返回 null 的工厂方法。 我们看到了很多代码,本意是想在调用不正确时抛出一个异常,但最终却返回了空。
Instead of:
与其这样写:
{:.bad} {% prettify dart tag=pre+code %} factory StreamReader(dynamic data) { StreamReader reader; if (data is ByteData) { reader = BlockReader(data); } else if (data is Map) { reader = JSONBlockReader(data); } return reader; } {% endprettify %}
Do:
不如这样:
{:.good} {% prettify dart tag=pre+code %} 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'); } } {% endprettify %}
If the intent of the factory was indeed to return null, then you can turn it
into a static method so it is allowed to return null
.
如果一个工厂方法的初衷就是可能返回空值,将其转为可返回 null
的静态方法是更好的选择。
The assert will be unnecessary when everything is fully migrated, but for now it is needed if you actually want to keep the check. Options:
对于完全迁移的代码而言,这个断言是不必要的,但是如果你希望保留该检查,那么它 也需要 留下。 几种方式可供你选择:
-
Decide that the assert is not really necessary, and remove it. This is a change in behavior when asserts are enabled.
确定是否真的需要这个断言,然后将其删除。 当断言启用时,这是一种行为上的变更。
-
Decide that the assert can be checked always, and turn it into
ArgumentError.checkNotNull
. This is a change in behavior when asserts are not enabled.确定断言始终会被检查,接着将其转换为
ArgumentError.checkNotNull
。 当断言未启用时,这是一种行为上的变更。 -
Keep the behavior exactly as is: add
// ignore: unnecessary_null_comparison
to bypass the warning.通过添加
//ignore: unnecessary_null_comparison
来绕过警告并且保持原有的行为。
An explicit runtime null check, for example if (arg == null) throw ArgumentError(...)
, will be flagged as an unnecessary comparison if you make
arg
non-nullable.
类似 if (arg == null) throw ArgumentError(...)
这样的显式空判断,
会在 arg
为非空时标记为不必要。
But, the check is still needed if the program is a mixed-version one. Until
everything is fully migrated and the code switches to running with sound null
safety, it will be possible for arg
to be null.
但是在混合模式下的程序 仍然需要 这样的判断。
在所有代码都迁移且运行在完全的空安全模式下前,arg
仍然可能为空。
The simplest way to preserve behavior is change the check into
ArgumentError.checkNotNull
.
保留这项行为的最简单的方法是将判断改为
ArgumentError.checkNotNull
。
The same applies to some runtime type checks. If arg
has static type String
, then if (arg is! String)
is actually checking
whether arg
is null
. It might look like migrating to null safety means arg
can never be null
, but it could be null
in unsound null safety. So, to preserve
behavior, the null check should remain.
运行时的检查同样适用。如果 arg
指定了静态类型为 String
,
那么 if (arg is! String)
实际上是在检查 arg
是否为 null
。
尽管代码在迁移到空安全后,arg
应该是永远不为 null
的,
但是在不完全的空安全中它仍有可能为 null
的。
Import package:collection
and use the extension method firstWhereOrNull
instead of firstWhere
.
导入 package:collection
package 并使用 firstWhereOrNull
扩展方法代替 firstWhere
。
Unlike the late final
suggestion above, these attributes cannot be marked as
final. Often, settable attributes also do not have initial values since they are
expected to be set sometime later.
与上文说到的 late final
的建议不同的是,这些字段不能被标记为终值。
通常,可被修改的属性也没有初始值,因为它们可能会在稍后才被赋值。
In such cases, you have two options:
在这样的情况下,你有两种选择:
-
Set it to an initial value. Often times, the omission of an initial value is by mistake rather than deliberate.
为其设置初始值。通常情况下,初始值未被设置是无意的错误,而不是有意为之。
-
If you are sure that the attribute needs to be set before accessed, mark it as
late
.如果你 确定 这个属性需要在访问之前被赋值,将它标记为
late
。WARNING: The
late
keyword adds a runtime check. If any user callsget
beforeset
they'll get an error at runtime.注意:
late
关键词会在运行时添加检查。 如果在set
之前调用了get
,会在运行时抛出异常。
The
lookup operator
on Map ([]
) by default returns a nullable type. There's no way to signal to
the language that the value is guaranteed to be there.
映射类型的
查询操作符
([]
) 返回的值默认是可空类型。
此处没有办法告诉 Dart ,返回的值一定是非空的。
In this case, you should use the bang operator (!
) to cast the value back to
V:
在这种情况下,你应该使用强制非空操作符 (!
) 将可空的类型转为非空 (V)。
return blockTypes[key]!;
Which will throw if the map returns null. If you want explicit handling for that case:
如果 map 返回了 null,则会抛出异常。如果你希望手动处理这些情况:
var result = blockTypes[key];
if (result != null) return result;
// Handle the null case here, e.g. throw with explanation.
It is typically a code smell to end up with nullable code like this:
下面这样以可空内容结尾的代码是一种典型的代码异味:
{:.bad} {% prettify dart tag=pre+code %} List<Foo?> fooList; // fooList can contain null values {% endprettify %}
This implies fooList
might contain null values. This might happen if you are
initializing the list with length and filling it in via a loop.
它隐含了 fooList
可能包含空值的信息。
在你以长度初始化列表并循环填入值时,这种情况可能会出现。
If you are simply initializing the list with the same value, you should instead
use the
filled
constructor.
如果你仅仅想要以相同的值初始化列表,你应该使用
filled
构造。
{:.bad} {% prettify dart tag=pre+code %} _jellyCounts = List<int?>(jellyMax + 1); for (var i = 0; i <= jellyMax; i++) { _jellyCounts[i] = 0; // List initialized with the same value } {% endprettify %}
{:.good} {% prettify dart tag=pre+code %} _jellyCounts = List.filled(jellyMax + 1, 0); // List initialized with filled constructor {% endprettify %}
If you are setting the elements of the list via an index, or you are populating each element of the list with a distinct value, you should instead use the list literal syntax to build the list.
如果你需要通过索引来设置元素,或者使用不同的值填充每个元素, 则应该使用列表的字面量表达式来构建列表。
{:.bad} {% prettify dart tag=pre+code %} _jellyPoints = List<Vec2D?>(jellyMax + 1); for (var i = 0; i <= jellyMax; i++) { _jellyPoints[i] = Vec2D(); // Each list element is a distinct Vec2D } {% endprettify %}
{:.good} {% prettify dart tag=pre+code %} _jellyPoints = [ for (var i = 0; i <= jellyMax; i++) Vec2D() // Each list element is a distinct Vec2D ]; {% endprettify %}
You may encounter this error:
你可能会遇到这样的错误:
The default 'List' constructor isn't available when null safety is enabled. #default_list_constructor
The default list constructor fills the list with null
, which is a problem.
默认的列表构造会将列表用 null
填充,会造成问题。
Change it to List.filled(length, default)
instead.
将它变为 List.filled(length, default)
即可。
I'm using package:ffi
and get a failure with Dart_CObject_kUnsupported
when I migrate. What happened?
Lists sent via ffi can only be List<dynamic>
, not List<Object>
or
List<Object?>
. If you didn't change a list type explicitly in your migration,
a type might still have changed because of changes to type inference that happen
when you enable null safety.
通过 ffi 发送的列表只能是 List<dynamic>
,
而不能是 List<Object>
或 List<Object?>
。
就算你未在迁移过程中手动更改类型,类型也可能会被改变,
因为启用空安全后,类型推导推算出了这样的结果。
The fix is to explicitly create such lists as List<dynamic>
.
手动创建 List<dynamic>
类型的列表可以解决这个问题。
The migration tool adds /* == false */
or /* == true */
comments when it
sees conditions that will always be false or true while running in sound mode.
Comments like these might indicate that the automatic migration is incorrect and
needs human intervention. For example:
空安全模式下,在当某个表达式的结果一定为 false 或 true 的时候,
迁移工具会自动添加 /* == false */
或者 /* == true */
这样的注释。
这样的注释将会导致自动迁移出现错误,并且需要人工干预。例如:
if (registry.viewFactory(viewDescriptor.id) == null /* == false */)
In these cases, the migration tool can't distinguish defensive-coding situations and situations where a null value is really expected. So the tool tells you what it knows ("it looks like this condition will always be false!") and lets you decide what to do.
在这些情况下,迁移工具无法区分防御性编码情况或是确实需要空值的情况。 那么该工具会告诉你「这看起来永远为 false!」并让你进行决定。
For a long time, the dart2js compiler has had optimizations specifically targeting null values and null checks. Because of that, null safety is not expected to affect much the output of the compiler.
长久以来,dart2js 编译器针对空值和空检查都有特殊的优化。 因此空安全并不会对编译产出带来太大影响。
A few notes that are worth highlighting:
依然有几点值得注意:
-
dart2js emits
!
null assertions, but you may not notice them. That's because pre null-safety dart2js already emitted null checks (so they are already in the existing programs).dart2js 添加了
!
空断言,但你可能不会注意到它。 这是因为 dart2js 早已支持了对空值进行检查(所以它们已在你的程序中存在)。 -
dart2js emits these null assertions regardless of sound/unsound null safety and regardless of optimization level. In fact, dart2js doesn't remove
!
when using-O3
or--omit-implicit-checks
.无论是健全或非健全的安全,又或是不同的优化等级,dart2js 都会添加这些空断言。 实际上在使用
-O3
或--omit-implicit-checks
时 dart2js 都不会移除!
。 -
dart2js may optimize away unnecessary null checks. This is because the same optimizations done by dart2js in the past will be able to eliminate unnecessary null checks when it knows the value is not null.
dart2js 可能会优化掉不必要的空检查。 这是因为 dart2js 过去在优化时,能够在值不为空时清除不必要的空检查。
-
By default dart2js would generate parameter subtype checks (runtime checks used to ensure covariant virtual calls are given appropriate arguments). These are elided with the
--omit-implicit-checks
flag just as before. Recall that this flag can make programs have unexpected behavior if types are invalid, so we continue to recommend that the code has strong test coverage to avoid any surprises. In particular, dart2js optimizes code based on the fact that inputs should comply with the type declaration. If the code provides arguments of an invalid type, those optimizations would be wrong and the program could misbehave. This was true for inconsistent types before, and is true with inconsistent nullabilities now with sound null-safety.默认情况下,dart2js 会生成参数子类型的检查 (用于确保协变的虚拟调用使用了合适的参数的运行时检查)。 与先前相同,使用
--omit-implicit-checks
会省略它们。 回想一下,如果类型无效,这个开关会让程序出现异常, 因此我们依然建议代码测试覆盖率尽可能地高,以避免任何事故。 特别是 dart2js 会基于传入值符合类型声明这一条件来优化代码。 如果代码提供了无效类型的参数,优化将不是正确的,导致程序异常。 这对于之前的类型不一致是正确的,对于现在健全的空安全的可空性不一致也是如此。 -
You may notice that DDC and the Dart VM have special error messages for null checks, but to keep applications small dart2js does not.
你可能会注意到 DDC 和 Dart VM 对于空检查的错误有比较特殊的错误提示, 但是为了保持应用的轻量体积,dart2js 并没有这样的提示。
-
You may see errors indicating that
.toString
is not found onnull
. This is not a bug, it is how dart2js has always encoded some null checks. That is, dart2js represents some null checks compactly by making an unguarded access of a property of the receiver. So instead ofif (a == null) throw
, it generatesa.toString
. ThetoString
method is defined in JavaScript Object and is a fast way to verify that an object is not null.你可能会看到
.toString
在null
上未找到的错误。 这不是一个 bug,是 dart2js 一直以来添加的空检查。 dart2js 通过对接收者对象属性的访问来压缩一些空检查。 它生成的是a.toString
而不是if (a == null) throw
。 在 JavaScript 对象中定义的toString
方法可以快速验证对象是否可空。If the very first action after a null check is an action that will crash when the value is null, dart2js can remove the null check and let the action cause the error.
如果空检查后的第一个行为是当值为空时会崩溃,dart2js 可以删除空检查并让动作抛出错误。
For example, a Dart expression
print(a!.foo());
could turn directly into:例如,
print(a!.foo());
语句可以直接转换为:P.print(a.foo$0());
This is because the call
a.foo$()
will crash ifa
is null. If dart2js inlinesfoo
, it will preserve the null check. So for example, iffoo
wasint foo() => 1;
the compiler might generate:这是因为调用
a.foo$()
会在a
为空时崩溃。 如果 dart2js 内联foo
,它将保留空检查。 例如,如果foo
是int foo() => 1;
,编译器可能会生成:a.toString; P.print(1);
If by chance the first thing the inlined method did was a field access on the receiver, for example
int foo() => this.x + 1;
, then again dart2js can remove the redundanta.toString
null check, just like non-inlined calls, and generate:如果内联方法做的第一件事是对接收者的字段访问,例如
int foo() => this.x + 1;
, 那么 dart2js 可以再次删除多余的a.toString
空检查,就像非内联调用一样生成:P.print(a.x + 1);