空安全:常见问题
- 在迁移代码时,我应该注意哪些运行时的改动? 
- 如果在测试中的值始终为 null 应该怎样处理? 
- 新的 required 和 @required 关键词有何异同? 
- 我应该如何迁移应该为 final 而目前并不是的字段? 
- 我应该如何迁移 built_value 类? 
- 我应该如何迁移可能返回 null 的工厂方法? 
- 我应该如何迁移现在提示无用的 assert(x != null)? 
- 我应该如何迁移现在提示不必要的运行时空判断? 
- Iterable.firstWhere 方法不再接受 orElse: () => null。 
- 我应该如何处理有 setters 的属性? 
- 我需要怎样标记映射的返回值为非空类型? 
- 为什么我的 List/Map 中的泛型是可空的? 
- 默认的 List 构造有什么改动? 
- 我在迁移使用了 package:ffi 的代码的时候遇到了 Dart_CObject_kUnsupported 的错误。发生了什么? 
- 为什么迁移工具在我的代码中添加了注释 
- 关于编译为 JavaScript 时的空安全我应该知道什么? 
- 资源 
此处涵盖了一些我们在迁移 Google 内部代码至 空安全 时遇到的常见问题。
在迁移代码时,我应该注意哪些运行时的改动?
在空安全迁移中的大部分影响,不会立刻出现在刚刚迁移完成的开发者身上:
- 
    静态的空安全检查,会在开发者完成迁移后立刻生效。 
- 
    完整的空安全检查,只会在所有代码都已迁移,并且启用了完整的空安全模式时生效。 
但是有两项例外你需要注意:
- 
    对于任何模式而言, !操作符都是在运行时进行的空检查。所以在进行迁移时,请确保你仅对null可能由混合模式造成污染的代码位置添加!,就算发起调用的代码还未迁移至空安全,也应如此。
- 
    late会在运行时检查。所以请你仅在确定它被使用前一定会被初始化的情况下使用late。
如果在测试中的值始终为 null 应该怎样处理?
如果在测试中某个值始终为 null,可以通过将测试传值和测试需要的值改为非空,来改进你的测试。
新的 required 和 @required 关键词有何异同?
@required 注解会将参数标记为必须传递。如果未传,分析器会给出一个提示。
有了空安全,非空类型的命名参数要么需要默认值,要么需要使用 required 关键字修饰。否则,它在未传递时默认会是 null,就显得不合理了。
在旧的代码中,required 关键词会被看作 @required 注解,即参数未传递时会显示一个分析器提示。
当你在空安全的代码中使用空安全代码时,如果 required 修饰的参数未传递,会显示一个错误。
那么它对于迁移来说意味着什么呢?当你给以前没有使用 @required 注解的参数加上 required 时要十分小心。任何没有传递新需要的参数的代码,都无法进行编译。实际上,你可以加上默认值,或是将参数变为可空类型。
我应该如何迁移应该为 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 的。在使用空安全的时候,你会发现想让它们的类型为非空,也不是一件容易的事。如果初始化的时机不合适,那么直到初始化前,它都只能是可空的类型。幸运的是,你还有其他选择:
- 
    将构造转变为工厂方法,并将其委托给一个直接初始化所有字段的真正的构造函数。在 Dart 中,这样的私有构造通常是一个下划线: _。如此一来,字段就可以是final且非空了。在空安全迁移介入 之前,你就可以这样进行调整。
- 
    或者,将字段标记为 late final。这会使得字段只被初始化一次。在它被读取之前必须被初始化。
我应该如何迁移 built_value 类?
使用了 @nullable 注解的 getter 应当直接转变为可空的类型,然后移除所有的 @nullable 注解。例如:
@nullable
int get count;
变成
int? get count; //  Variable initialized with ?
就算迁移工具建议,没有 使用 @nullable 注解的 getter 也不应该是可空的类型。这时可以根据需要添加 ! 操作符,并且重新进行分析。
我应该如何迁移可能返回 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 的静态方法是更好的选择。
我应该如何迁移现在提示无用的 assert(x != null)?
对于完全迁移的代码而言,这个断言是不必要的,但是如果你希望保留该检查,那么它 也需要 留下。几种方式可供你选择:
- 
    确定是否真的需要这个断言,然后将其删除。当断言启用时,这是一种行为上的变更。 
- 
    确定断言始终会被检查,接着将其转换为 ArgumentError.checkNotNull。当断言未启用时,这是一种行为上的变更。
- 
    通过添加 //ignore: unnecessary_null_comparison来绕过警告并且保持原有的行为。
我应该如何迁移现在提示不必要的运行时空判断?
如果 arg 为非空时,编译器会在运行时将显式空安全判断标记为非必要。
if (arg == null) throw ArgumentError(...)`
混合模式下的程序必须包含这样的判断。在所有代码都迁移且运行在完全的空安全模式下前,
arg 仍然可能为 null。
保留这项行为的最简单的方法是将判断改为
ArgumentError.checkNotNull。
运行时的检查同样适用。如果 arg 指定了静态类型为 String,那么 if (arg is! String) 实际上是在检查 arg 是否为 null。尽管代码在迁移到空安全后,arg 应该是永远不为 null 的,但是在不完全的空安全中它仍有可能为 null 的。
Iterable.firstWhere 方法不再接受 orElse: () => null。
导入 package:collection package 并使用 firstWhereOrNull 扩展方法代替 firstWhere。
我应该如何处理有 setters 的属性?
与上文说到的 late final 的建议不同的是,这些字段不能被标记为终值。通常,可被修改的属性也没有初始值,因为它们可能会在稍后才被赋值。
在这样的情况下,你有两种选择:
- 
    为其设置初始值。通常情况下,初始值未被设置是无意的错误,而不是有意为之。 
- 
    如果你 确定 这个属性需要在访问之前被赋值,将它标记为 late。注意: late关键词会在运行时添加检查。如果在set之前调用了get,会在运行时抛出异常。
我需要怎样标记映射的返回值为非空类型?
映射类型的
查询操作符
([]) 返回的值默认是可空类型。此处没有办法告诉 Dart ,返回的值一定是非空的。
在这种情况下,你应该使用强制非空操作符 (!) 将可空的类型转为非空 (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 可能包含空值的信息。在你以长度初始化列表并循环填入值时,这种情况可能会出现。
如果你仅仅想要以相同的值初始化列表,你应该使用
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 */)
在这些情况下,迁移工具无法区分防御性编码情况或是确实需要空值的情况。那么该工具会告诉你「这看起来永远为 false!」并让你进行决定。
关于编译为 JavaScript 时的空安全我应该知道什么?
空安全带来了代码体积减小及性能提升等优化。表面上 Flutter 编译为原生端的构建的优化会更加明显,例如 AOT。我们先前已经在 Web 的生产构建器上已经引入了一些类似空安全的优化。所以,Web 应用上的变化可能并不如原生端明显。
依然有几点值得注意:
- 
    生产环境下的 JavaScript 编译器会生成 !空断言,在比较输出的时候你可能不会注意到它。这是因为编译器已支持了对空值进行检查。
- 
    无论是健全或非健全的安全,又或是不同的优化等级,编译器都会生成这些空断言,实际上在使用 -O3或--omit-implicit-checks时编译器都不会移除!。
- 
    生产环境下的 JavaScript 编译器可能会移除没有必要的空检查,一般会发生在生产环境下的 Web 编译器在空安全之前做了一些优化,当它知道值不为空的时候,就删除了这些检查。 
- 
    默认情况下,编译器会生成参数子类型的检查。(用于确保协变的虚拟调用使用了合适的参数的运行时检查)。与先前相同,使用 --omit-implicit-checks编译器会省略它们。回想一下,如果类型无效,这个开关会让程序出现异常,因此我们依然建议代码测试覆盖率尽可能地高,以避免任何事故。特别是编译器会基于传入值符合类型声明这一条件来优化代码。如果代码提供了无效类型的参数,优化将不是正确的,导致程序异常。这对于之前的类型不一致是正确的,对于现在健全的空安全的可空性不一致也是如此。
- 
    你可能会注意到开发版的 JavaScript 编译器和 Dart VM 对于空检查的错误有比较特殊的错误提示,但是为了保持应用的轻量体积,生产环境下的编译器并没有这样的提示。 
- 
    你可能会看到 .toString在null上未找到的错误。这不是一个 bug,是编译器一直以来添加的空检查。编译器会通过对接收者对象属性的访问来压缩一些空检查。它生成的是a.toString而不是if (a == null) throw。在 JavaScript 对象中定义的toString方法可以快速验证对象是否可空。如果空检查后的第一个行为是当值为空时会崩溃,编译器可以删除空检查并让动作抛出错误。 例如, print(a!.foo());语句可以直接转换为:P.print(a.foo$0());这是因为调用 a.foo$()会在a为空时崩溃。如果 dart2js 内联foo,它将保留空检查。例如,如果foo是int foo() => 1;,编译器可能会生成:a.toString; P.print(1);如果内联方法做的第一件事是对接收者的字段访问,例如 int foo() => this.x + 1;,那么 dart2js 可以再次删除多余的a.toString空检查,就像非内联调用一样生成:P.print(a.x + 1);