Dart空安全终极指南

2022-09-20 16:51:27 浏览数 (2)

Dart中的空安全

Null Safety 的引入是 Dart 语言的一个重要里程碑。Null Safety 通过「在开发期间而不是在运行时捕获 null 错误来」帮助您避免一整类问题。

本文将通过一些例子来展示如何使用新的 Null Safety 功能。

❝Null Safety 可作为 Flutter 2.0 的稳定版本使用,并且默认为所有使用 Flutter 2.2 创建的项目启用。 ❞

Dart的类型系统

Dart 有一个「健全的类型系统」。当我们编写 Dart 代码时,「类型检查器」会确保我们不能编写下面这样的代码:

代码语言:javascript复制
int age = "hello world"; // A value of type `String` can't be assigned to a variable of type `int`

此代码产生一个错误,告诉我们*“String不能将值分配给类型为变量的变量int”*。

同样,当我们在 Dart 中编写函数时,可以指定返回「类型」

代码语言:javascript复制
int square(int value) {
  return value * value;
}

由于「类型安全」,Dart 可以 100% 保证这个函数「总是」返回一个int.

但是类型安全本身并不能保证变量(或返回值)不是null.

所以下面的代码编译正常,但「在运行时」生成异常:

代码语言:javascript复制
square(null);
// Unhandled Exception: NoSuchMethodError: The method '*' was called on null.

在这个例子中,很容易发现问题。但在大型项目中,很难跟踪什么可以和不可以是null

要判断是否为null 可能要添加很多额外的代码:

代码语言:javascript复制
int square(int value) {
  assert(value != null); // for debugging
  if (value == null) throw Exception();
  return value * value;
}

现在,我们有了 一个更好的解决方案。

Dart Null Safety

Dart 2.12默认启用空安全,启用空安全会有如下3个好处:

  • 我们可以编写「编译时」保证的强壮的空安全代码。可以让我们更有效率,因为 Dart 可以告诉我们什么时候做错了。
  • 可以更容易地声明我们的**意图,**这样的API 更易于使用。
  • Dart 编译器可以优化我们的代码,从而生成更小更快的程序。

下面,我们去看看 Null Safety 是如何工作的。

声明不可为空的变量

主要的变化是现在所有类型「默认情况下」都是不可为空。

代码语言:javascript复制
void main() {
  int age; // non-nullable
  age = null; // A value of type `Null` can't be assigned to a variable of type 'int'
}

这段代码是无法编译的。

当使用不可为空的变量时,我们必须遵守一个重要的原则:

❝不可为空的变量必须始终使用非空值进行初始化。 ❞

牢记这个原创,我们对新语法就更容易理解了。

我们再看下上面那个例子:

代码语言:javascript复制
int square(int value) {
  return value * value;
}

value和返回值都保证不是null,因此 当我们在传null值时编译期间就会报错。

代码语言:javascript复制
square(null);
// The argument type 'Null' can't be assigned to the parameter type 'int'

如果有时候我们就像声明可为空的变量,要怎么办呢?

声明可为空变量

声明可为空变量需要使用 标识符

代码语言:javascript复制
String? name;  // initialized to null by default
int? age = 36;  // initialized to non-null
age = null; // can be re-assigned to null

❝注意:不需要在使用之前初始化可为空的变量。默认初始化为null。 ❞

以下是声明可空变量的其他方法:

代码语言:javascript复制
// nullable function argument
void openSocket(int? port) {
  // port can be null
}

// nullable return type
String? lastName(String fullName) {
final components = fullName.split(' ');
  return components.length > 1 ? components.last : null;
}

// using generics
T? firstNonNull<T>(List<T?> items) {
  // returns first non null element in list if any
  return items.firstWhere((item) => item != null);
}

断言运算符

但是在某些情况下,我们知道某个变量不可能null,但我们无法向编译器「证明」它。在这种情况下,可以使用断言运算符。

代码语言:javascript复制
int? maybeValue = 42;
int value = maybeValue!; // valid, value is non-nullable

这样,我们相当于告诉Dart,maybeValue是非空的,可以将它赋值给非空变量value。

请注意,将断言运算符应用于null值将引发运行时异常:

代码语言:javascript复制
String? name;
print(name!); // NoSuchMethodError: '<Unexpected Null Value>'
print(null!); // NoSuchMethodError: '<Unexpected Null Value>'

所以,当断言错误时,! 将引发运行时异常。

有时我们需要使用返回可空值的函数:

代码语言:javascript复制
String? lastName(String fullName) {
  final components = fullName.split(' ');
  return components.length > 1 ? components.last : null;
}

所以返回值需要赋值给一个非空变量:

代码语言:javascript复制
// prefer this:
String last = lastName('Andrea Bizzotto')!;
// to this:
String? last = lastName('Andrea Bizzotto');

总结一下:

  • 尽量使用不可为空的变量,这样就会将很多null错误拦截在「编译时」
  • 如果知道可以为空的表达式不会是null,则可以使用运算符将其分配给不可为空的变量。

非空和空的一些使用技巧

在dart中我们一定要添加对null的检查,这样我们的代码才能更健壮。

代码语言:javascript复制
int absoluteValue(int? value) {
  if (value == null) {
    return 0;
  }
  // if we reach this point, value is non-null
  return value.abs();
}

这样,我们在返回值时就可以使用value.abs() ,不用使用value?.abs()

我们再看看如下的代码

代码语言:javascript复制
int sign(int x) {
  int result; // non-nullable
  print(result.abs()); // invalid: 'result' must be assigned before it can be used
  if (x >= 0) {
    result = 1;
  } else {
    result = -1;
  }
  print(result.abs()); // ok now
  return result;
}

在result被初始化前使用就会报错,result在被使用前被赋值为非空,dart就不会报错。

在类中使用不可为空变量

如果类中的实例变量不可为空,则必须对其进行初始化:

代码语言:javascript复制
class BaseUrl {
  String hostName; // Non-nullable instance field 'hostName' must be initialized

  int port = 80; // ok
}

如果不能使用默认值初始化,则可以使用构造函数对其进行设置:

代码语言:javascript复制
class BaseUrl {
  BaseUrl(this.hostName);
  String hostName; // now valid

  int port = 80; // ok
}

不可为空的命名和位置参数

使用 Null Safety,必须始终「要求传递」不可为空的「命名」参数或设置「默认值」

一般函数以及类构造函数,都遵循以上原则:

代码语言:javascript复制
void printAbs({int value}) {  // 'value' can't have a value of null because of its type, and no non-null default value is provided
  print(value.abs());
}

class Host {
  Host({this.hostName}); // 'hostName' can't have a value of null because of its type, and no non-null default value is provided
  final String hostName;
}

我们可以使用新的修饰符required来修复上面的代码:

代码语言:javascript复制
void printAbs({required int value}) {
  print(value.abs());
}

class Host {
  Host({required this.hostName});
  final String hostName;
}

这样我们在使用时,编译器可以轻松识别出一些低级错误:

代码语言:javascript复制
printAbs(); // The named parameter 'value' is required, but there's no corresponding argument
printAbs(value: null); // The argument type 'Null' can't be assigned to the parameter type 'int'
printAbs(value: -5); // ok

final host1 = Host(); // The named parameter 'hostName' is required, but there's no corresponding argument
final host2 = Host(hostName: null); // The argument type 'Null' can't be assigned to the parameter type 'String'
final host3 = Host(hostName: "example.com"); //

如果使用可为空的变量,则reqired可以省略:

代码语言:javascript复制
class Host {
  Host({this.hostName});
  final String? hostName; // nullable, initialized to `null` by default
}
// all valid cases
final host1 = Host(); // hostName is null
final host2 = Host(hostName: null); // hostName is null
final host3 = Host(hostName: "example.com");

「位置」参数遵循相同的规则:

代码语言:javascript复制
class Host {
  Host(this.hostName); // ok
  final String hostName;
}

class Host {
  Host([this.hostName]); // The parameter 'hostName' can't have a value of 'null' because of its type, and no non-null default value is provided
  final String hostName;
}

class Host {
  Host([this.hostName = "www.codewithandrea.com"]); // ok
  final String hostName;
}

class Host {
  Host([this.hostName]); // ok
  final String? hostName;
}

最后,我们必须牢记一个黄金原则就不会出错:

❝不可为空的变量必须始终使用非空值进行初始化。 ❞

空感知级联运算符

为了处理 Null Safety,级联运算符现在有一个新的 变体:?... 例子:

代码语言:javascript复制
Path? path;
// will not do anything if path is null
path
  ?..moveTo(0, 0)
  ..lineTo(0, 2)
  ..lineTo(2, 2)
  ..lineTo(2, 0)
  ..lineTo(0, 0);

上面的级联操作只有在path 不为空时才会执行。

空感知下表运算符

之前的用法:

代码语言:javascript复制
int? first(List<int>? items) {
  return items != null ? items[0] : null; // null check to prevent runtime null errors
}

Dart 2.9后可以使用如下

代码语言:javascript复制
int? first(List<int>? items) {
  return items?[0]; 
}

late关键词

late关键字在「第一次读取」时初始化变量,而不是在「创建」时初始化。看下面的例子:

代码语言:javascript复制
class ExampleState extends State {
  late final TextEditingController textEditingController;

  @override
  void initState() {
    super.initState();
    textEditingController = TextEditingController();
  }
}

现在我们可以简化成如下:

代码语言:javascript复制
class ExampleState extends State {
  // late - will be initialized when first used (in the build method)
  late final textEditingController = TextEditingController();
}

通常latefinal,结合使用,将「只读」变量的创建「推迟」到首次读取时。

当初始化时会做很多任务时,这样做很理想:

代码语言:javascript复制
late final taskResult = doHeavyComputation();

在函数体内使用时:

代码语言:javascript复制
void foo() {
  late final int x;

  x = 5; // ok
  x = 6; // The late final local variable is already definitely initialized
}

不太建议以这种方式使用late变量。因为这样会导致不明显的运行时错误。例子:

代码语言:javascript复制
class X {
  late final int x;
  
  void set1() => x = 1;
  
  void set2() => x = 2;
}

void main() {
  X x = X();
  x.set1();
  print(x.x);
  x.set2(); // LateInitializationError: Field 'x' has already been initialized.
  print(x.x);
}

但我建议对late谨慎使用,并始终在用late声明变量时对其进行初始化。

静态和全局变量

除了late,所有全局变量「必须在声明时初始化,」

代码语言:javascript复制
int global1 = 42; // ok

int global2; // The non-nullable variable 'global2' must be initialized

late int global3; // ok

静态类变量也是同样的:

代码语言:javascript复制
class Constants {
  static int x = 10; // ok
  static int y; // The non-nullable variable 'y' must be initialized
  static late int z; // ok
}

最后

Null Safety 是 Dart 语言的一个重大变化,它可以帮助你编写更好、更安全的代码,「只要你正确使用它」

0 人点赞