Flutter 2.0 Null-Safety(空安全)使用和理解

本文介绍了Flutter 2.0中的Dart空安全特性,包括非空安全、空安全的概念,展示了如何使用可空类型、空值断言操作符,以及`late`关键字的延迟初始化。通过实例解析了空安全的重要性以及在代码中的应用,帮助开发者理解和应用空安全来提升代码质量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在 Flutter 2.0 中,一项重要的升级就是 Dart 支持 空安全,空安全究竟是什么?日常开发中我们该怎么使用?下面我们通过几个简单的代码来介绍 Flutter 空安全。


Dart 空安全是什么?

Dart 在语法上支持空安全检查。在空安全的代码编译期下,运行时的NullPointerException错误提示被提前到了开发阶段。

如下案例:

void main() {
  String mName;

  print(mName.length);
}

非空安全下,这段代码在 在编译阶段不会有任何提示。如果我们允许这样的代码运行,那么它将毫无疑问地崩溃。因为系统只允许你访问在原有类型和 Null 类下同时定义的方法和属性,比如 toString()== 和 hashCode 可以访问。

原因:变量 mName 在没有赋值时值是 null 类型。当调用null类型下的 .length 方法时便导致程序异常,因为null类型下根本不存在.length方法。

空安全下,在编译期便给出了报错提示,开发者可以及时进行修复。

可查看官方:健全的空安全 | Dart     


非空安全

在空安全推出之前,静态类型系统允许所有类型的表达式中的每一处都可以有 null

从类型理论的角度来说,Null 类型被看作是所有类型的子类;

在非空安全下,你可以对 List 类型调用 .add()或 []。如果是 int 类型,你可以对其调用 +。但是 null 值并没有它们定义的任何一个方法。所以当 null 被传递至其他类型的表达式时,任何操作都会失败。这就是空引用的症结所在——所有错误都来源于尝试在 null 上查找一个不存在的方法或属性。 


空安全

空安全通过修改了类型的层级结构,从根源上解决了这个问题。 Null 类型仍然存在,但它不再是所有类型的子类。现在的类型层级看起来是这样的:

既然 Null 已不再被看作所有类型的子类,那么除了特殊的 Null 类型允许传递 null 值,其他类型均不允许。

我们已经将所有的类型设置为 默认不可空 的类型。如果你的变量是 String 类型,它必须包含 一个字符串。这样一来,我们就修复了所有的空引用错误。


使用可空类型

如果 null 对我们来说没有什么意义的话,那大可不必再研究下去了。但实际上 null 十分有用,所以我们仍然需要合理地处理它。可选参数就是非常好的例子。让我们来看下这段空安全的代码:

makeCoffee(String coffee, [String? dairy]) {

  if (dairy != null) {
    print('$coffee with $dairy');
  } else {
    print('Black $coffee');
  }
}

此处我们希望 dairy 参数能传入任意字符串,或者一个 null 值。为了表达我们的想法,我们在原有类型 String 的尾部加上?,使得 dairy 成为可空的类型

本质上,这和定义了一个原有类型加 Null 的 组合类型 没有什么区别。所以如果 Dart 包含完整的组合类型定义,即 String? 就是 String|Null 的缩写。 

如果一个函数接受 String?,那么向其传递 String 是允许的,不会有任何问题。在此次改动中,我们将所有的可空类型作为基础类型的超类。你也可以将 null 传递给一个可空的类型,即 Null 也是任何可空类型的子类:


隐示\显示转换

但将一个可空类型传递给非空的基础类型,是不安全的。比如,你声明了一个 String 的非空类型变量,然后用这个变量接收会在你传递的值,很可能会去调用这个值的 String 类型下的方法|属性。

但是,如果你传递了 String?,传入的 null 就将可能产生错误:

requireStringNotNull(String definitelyString) {
  print(definitelyString.length);
}

main() {
  String? maybeString = null; // Or not!
  requireStringNotNull(maybeString);
}

这种不安全的程序是不会允许出现的。

然而,隐式转换 在 Dart 中一直存在。假设你将类型为 Object 的实例传递给了需要 String 的函数,编译器为 requireStringNotObject() 的参数静默添加了 as String 强制转换。

main() {
  Object maybeString = 'it is';
  requireStringNotObject(maybeString);
}

虽然,在编译时,Dart是允许这样的操作,检查器也不会报红提示。但是,在运行时进行转换很可能会抛出异常,隐式转换允许你给需要非空的 String 内容传递一个可空 String?。

所以,自动隐式转换与我们的安全性目标不符。在空安全时代,完全移除了隐式转换

为了保持健全性,你需要自己添加显式类型转换

requireStringNotObject(String definitelyString) {
  print(definitelyString.length);
}

main() {
  Object maybeString = 'it is';
  requireStringNotObject(maybeString as String);//显示转换
}

 顶层及底层

如果在一张有向图的顶部有是一个单一的超类(直接或间接),那么这个类型称为 顶层类型。类似的,如果有一个在底部有一个类型,是所有类型的子类,那么这个类型就被称为 底层类型

如果类型系统中有顶层和底层类型,将给我们带来一定程度的便利。在空安全引入以前,Dart 中的顶层类型是 Object,底层类型是 Null。由于空安全时代, Object 不再可空,所以它不再是一个顶层类型了,Null 也不再是它的子类。

Dart 中没有 令人熟知的 顶层类型。如果你需要一个顶层类型,可以用 Object?。同样的,Null 也不再是底层类型,否则所有类型都仍将是可空。取而代之是一个全新的底层类型 Never

依据实际开发中的经验,这意味着:

  • 如果你想表明让一个值可以接受任意类型,请用 Object? 而不是 Object。使用 Object 后会使得代码的行为变得非常诡异,因为它意味着能够是“除了 null 以外的任何实例”。

  • 在极少数需要底层类型的情况下,请使用 Never 代替 Null。如果你不了解是否需要一个底层类型,那么你基本上不会需要它。


空值断言​​​​​​​操作符

利用流程分析,将可空的变量转移到非空的一侧,是安全可靠的。你可以在先前可空的变量上调用断言方法,同时还能享受到非空类型的安全和性能优势。

但为确保这个行为安全性,开发者必须要理解变量与值的可空性之间的联系,因为类型检查器看不出这种联系。换句话说,作为代码的人类维护者,我们知道在使用 一个变量 时,它的值一定不会是 null,这时我们就可以对其进行断言。

通常你可以通过使用 as 转换来断言类型,这里你也可以这样做:

final String? error;
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${(error as String).toUpperCase()}';
}

“排除可空性的转换”的场景频繁出现,这促使了我们带来了新的短小精悍的语法。一个作为后缀的感叹号标记 (!) 会让左侧的表达式转换成其对应的非空类型。所以上面的函数等效于:​​​​​​​

String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${error!.toUpperCase()}';
}

关键字: required

我们在开发中,调用方法传参数时会遇到一个问题,即:可选参数是可以不传的

如果参数是可空的,那么还好。如果遇到不可空的就麻烦了:

Demon invoke(Target target, {String way, String? material}) {
    //...
}

当遇到上面的情况时,而我们又确实需要使用这个变量,使用required关键字修改后:

Demon invoke(Target target, {required String way, String? material}) {
    ...
}

//调用时,如果不传, 或者传入null,编辑器将会报错 
invoke(a); //错误
invoke(a, way: null); // 错误

延迟初始化 late

class Weather {
  late int _temperature = _readThermometer();
}

当你这么声明时,会让变量的初始化赋值 延迟 到字段首次被访问时。

换句话说,它让字段的初始化方式变得与顶层变量和静态字段完全一致。一般使用场景,是在当初始化表达式比较消耗性能,并且有可能不需要时,这会变得非常有用。

比如,通常实例字段的初始化内容无法访问到 this,因为在所有的初始化方法完成前,你无法访问到新的实例对象。但是,使用了 late 让这个条件不再为真,所以你 可以 访问到 this、调用方法以及访问实例的字段。

late 修饰符的强制约束

处理类似延迟初始化的常见行为,late可以告诉编辑器:这个非空变量,我稍后会初始化。

class Coffee {
  late String _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }
   
//使用
  String serve() => _temperature + ' coffee';
}

此处我们注意到,_temperature 字段是一个非空的类型,但是并没有进行初始化。同时,在使用时也没有明确的空断言。当前场景里,字段并不一定已经被初始化,每次它被读取时,都会插入一个运行时的检查,以确保它已经被赋值。如果并未赋值,就会抛出一个异常

 late 修饰符是“在运行时而非编译时对变量进行约束”。这就让 late 这个词语约等于 何时 执行对变量的强制约束。给一个变量 + String 类型就是在说:“我的值绝对是字符串”。而加上 late 修饰符意味着:“每次运行都要检查是不是真的”。

如果在 编译时 为它赋值为 null 或 String? 就会出错。虽然 late 修饰符让你延迟了初始化,但它仍然禁止你将变量作为可空的类型进行处理

late延迟的终值

你也可以将 late 与 final 结合使用:

class Coffee {
  late final String _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  String serve() => _temperature + ' coffee';
}

与普通的 final 字段不同,你不需要在声明或构造时就将其初始化。

但是你仍然只能对其进行 一次 赋值,并且它在运行时会进行校验。如果你尝试对它进行多次赋值,比如 heat() 和 chill() 都调用,那么第二次的赋值会抛出异常。

这是确定字段状态的好方法,它最终会被初始化,并且在初始化后是无法改变的。

换句话说,新的 late 修饰符与 Dart 的其他变量修饰符结合后,已经实现了 Kotlin 中的 lateinit 和 Swift 中的 lazy 的大量特性。如果你需要给局部变量加上一些延迟初始化,你也可以在局部变量上使用它。


参考资料:

深入理解空安全 | Dart

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艾阳Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值