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
可能要添加很多额外的代码:
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值时编译期间就会报错。
square(null);
// The argument type 'Null' can't be assigned to the parameter type 'int'
如果有时候我们就像声明可为空的变量,要怎么办呢?
声明可为空变量
声明可为空变量需要使用?
标识符
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
,但我们无法向编译器「证明」它。在这种情况下,可以使用断言运算符。
int? maybeValue = 42;
int value = maybeValue!; // valid, value is non-nullable
这样,我们相当于告诉Dart,maybeValue是非空的,可以将它赋值给非空变量value。
请注意,将断言运算符应用于null
值将引发运行时异常:
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
来修复上面的代码:
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,级联运算符现在有一个新的 变体:?..
. 例子:
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
关键字在「第一次读取」时初始化变量,而不是在「创建」时初始化。看下面的例子:
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();
}
通常late
和final
,结合使用,将「只读」变量的创建「推迟」到首次读取时。
当初始化时会做很多任务时,这样做很理想:
代码语言: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,
所有全局变量「必须在声明时初始化,」
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 语言的一个重大变化,它可以帮助你编写更好、更安全的代码,「只要你正确使用它」。