空安全练习

382 阅读5分钟

本篇文章翻译自 👉Null safety codelab,算是在学习Dart空安全时的资料了。

本文主要为您介绍Dart的空安全类型系统,空安全是在Dart的2.12版本引入的。当您选择使用空安全的时候,代码的类型就默认是非空的了,这就意味着值不能是null的。

本文主要涵盖了以下的内容:

  • 可空和不可空的类型
  • 什么时候添加 ? 来表明可空,什么时候添加 ! 来表明不可空
  • 流程分析和类型提升
  • late 关键字是如何影响变量和初始化的

可以使用嵌入的DartPad编辑器来运行练习代码,为了更好的吸收本节的知识,需要您对Dart基本语法有一定的了解。

注意:   本片文章使用DartPads展示案例和练习代码。如果你在页面内没看到可以执行的DartPads,可以去DartPad troubleshooting page练习。

可空和不可空类型

当您选择使用空安全的时候,所有的类型都是默认都是非空的了。举个例子🌰,假如有一个 String 类型的变量,那么它一定是非空的,也就是说肯定有值。

如果你想一个不可空类型的变量,既可以接受非空值,又可以接受null值,那么可以在不可控类型的名字后面添加一个小问号 (?) 。比如,String? 的变量既可以接受字符串,也可以接受null。

练习: 不可空类型

下面的变量 a 被声明为 int,尝试把 a 的赋值为3 或者 145,但是不能赋值为null!

void main() {
  int a;
  a = 3;
  print('a is $a.');
}

如果给a赋值为null======》a=null,则会在编辑器提示:

line 3 • A value of type 'Null' can't be assigned to a variable of type 'int'.

Try changing the type of the variable, or casting the right-hand type to 'int'.

练习: 可空类型

如果您需要一个变量,可以接受一个null的值怎么办呢?尝试改变 a 的类型,使 a 可以接受null值或者int值:

测试代码:

void main() {
  int a;
  a = null;
  print('a is $a.');
}

练习: 泛型类型参数

泛型的类型参数也可以是可空的或者非空的,尝试使用小问号(可空)标记来改正 aNullableListOfStrings and aListOfNullableStrings 的类型声明:

测试代码:

void main() {
  List<String> aListOfStrings = ['one', 'two', 'three'];
  List<String> aNullableListOfStrings;
  List<String> aListOfNullableStrings = ['one', null, 'three'];

  print('aListOfStrings is $aListOfStrings.');
  print('aNullableListOfStrings is $aNullableListOfStrings.');
  print('aListOfNullableStrings is $aListOfNullableStrings.');
}

空断言操作符 (!)

如果您确保一个可空类型的表达式是非空的,可以使用空断言操作符 (!) 来让Dart将表达式视为非空的。在表达式后面加上 ! 操作符,是告诉Dart这个值不会为null,并且可以安全的将值赋值给不可空的变量。

⚠️如果您判断错了, Dart会抛出一个运行时的异常。这就让 ! 操作符是不安全的,因此最好是在确保是非空的情况下,才使用 ! 。

练习: 空断言

在下面的代码中,尝试添加!来修正错误的语句:

//测试代码

int? couldReturnNullButDoesnt() => -3;

void main() {
  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];

  int a = couldBeNullButIsnt;
  int b = listThatCouldHoldNulls.first; // first item in the list
  int c = couldReturnNullButDoesnt().abs(); // absolute value

  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}

错误的地方:

line 8(b赋值的地方) • A value of type 'int?' can't be assigned to a variable of type 'int'.

Try changing the type of the variable, or casting the right-hand type to 'int'.

line 9(c赋值的地方) • The method 'abs' can't be unconditionally invoked because the receiver can be 'null'.

Try making the call conditional (using '?.') or adding a null check to the target ('!').

类型提升

由于空安全的支持,Dart的流程分析已经将扩展到考虑可空性。类型提升是指将非空值的可空对象视为不可空类型的对象。

练习: 明确赋值

Dart的类型系统可以追踪到变量是在哪里赋值的,是在哪里读取的,并且可以在变量被读取之前验证不可空变量是否已经赋值。这个过程叫做明确赋值

尝试取消👇下面代码中 if-else 表达式的注释,然后看出现的分析错误:

测试代码:

void main() {
  String text;

  //if (DateTime.now().hour < 12) {
  //  text = "It's morning! Let's make aloo paratha!";
  //} else {
  //  text = "It's afternoon! Let's make biryani!";
  //}

  print(text);
  print(text.length);
}

分析错误:

line 10(第一个print处) • The non-nullable local variable 'text' must be assigned before it can be used.

Try giving it an initializer expression, or ensure that it's assigned on every execution path.

line 11(第二个print处) • The non-nullable local variable 'text' must be assigned before it can be used.

Try giving it an initializer expression, or ensure that it's assigned on every execution path.

原因就是我们有给text字段赋值,取消注释之后,就会错误就会消失。

练习: 空检查

下面的代码中,添加一个 if 表达式在 getLength 方法体中最开始的地方,if表达式的处理是,如果入参 str 为null,那么返回0。

测试代码如下:

int getLength(String? str) {
  // Add null check here

  return str.length;
}

void main() {
  print(getLength('This is a string!'));
}

注意⚠️,添加了if的表达式之后,后面就不需要在判断 str 是否为 null 了。

练习: 异常下的提升

return表达式也可以使用异常提升,尝试添加空检查,在null的时候抛出 Exception 异常,而不是返回0。

测试代码:

int getLength(String? str) {
  return str.length;
}

void main() {
  print(getLength(null));
}

这个代码是有错的,因为没有处理null的情况,只需要在if中throw异常。

late 关键字

有时在一个类中的字段、变量、顶层变量应该是不可空的,但是不能立即赋值。对于这种情况,就可以使用late 关键字.

如果您在变量声明之前添加了 late ,相当于告诉 Dart:

  • 先不给变量赋值
  • 可以稍候再给变量赋值
  • 确保在使用之前变量是赋完值的

If you declare a variable late and the variable is read before it’s assigned a value, an error is thrown. 如果给一个变量标记为 late 的,但是在读取(使用)变量的时候,还没有完成赋值,就会抛出异常。

练习: 使用 late

尝试使用 late 关键字来修正下面的代码。 为了额外的小乐趣,可以尝试注释掉 description这一行。

测试代码:

class Meal {
  String _description;

  set description(String desc) {
    _description = 'Meal description: $desc';
  }

  String get description => _description;
}

void main() {
  final myMeal = Meal();
  myMeal.description = 'Feijoada!';
  print(myMeal.description);
}

错误的地方:_description 字段的初始化在set方法中,因此可以将它标记为 late

练习: Late 循环引用

late 关键字对像循环引用这样的复杂模式非常有帮助,下面的代码有两个对象,他们互相持有对方的引用。尝试用 late 关键字来fix下面的代码。

注意⚠️:您可以将 latefinal 组合使用,只要对 fianl 的变量赋值一次之后,它就变为只读的了,再次赋值就会报错。

测试代码:

class Team {
  final Coach coach;
}

class Coach {
  final Team team;
}

void main() {
  final myTeam = Team();
  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;

  print('All done!');
}

注意 myTeammyCoach 互相支持对方,也没有在初始化的赋值,所以是可以添加 late 的。

练习: Late and lazy

late 的另外一个帮助是:延迟初始化 昂贵的不可空字段。尝试:

  • 不改动代码的情况下,运行代码查看输出
  • 考虑一下:如果将 _cache 修改为 late 字段会发生什么
  • 将 _cache 改为 late ,并运行代码,看看是不是您想的

测试代码:

int _computeValue() {
  print('In _computeValue...');
  return 3;
}

class CachedValueProvider {
  final _cache = _computeValue();
  int get value => _cache;
}

void main() {
  print('Calling constructor...');
  var provider = CachedValueProvider();
  print('Getting value...');
  print('The value is ${provider.value}!');
}

输出的顺序会发生变化,加完late之后,会在初次使用的初始化,而不加的话,会在构造类的时候初始化。

有趣的现象: _cache字段添加了 late 之后,将 _computeValue 方法从顶层方法与移到类内部,依然可以运行。late 关键字标注的变量的初始化表达式可以使用类实例的方法。

后面是什么呢?

很好啦~~,您已经学完了本节内容!如果想要学更多内容,下面有一些建议: