本篇文章翻译自 👉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下面的代码。
注意⚠️:您可以将 late 和 final 组合使用,只要对 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!');
}
注意 myTeam 和 myCoach 互相支持对方,也没有在初始化的赋值,所以是可以添加 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关键字标注的变量的初始化表达式可以使用类实例的方法。
后面是什么呢?
很好啦~~,您已经学完了本节内容!如果想要学更多内容,下面有一些建议:
-
空安全的更多知识:
- 总览: Sound null safety.
- 深入理解: Understanding null safety.
-
尝试其他的 codelab.