快速上手 Flutter 空安全

导语

在 Flutter 2.0 中,一项重要的升级就是 Dart 支持 空安全。Alex 为我们贴心地翻译了多篇关于空安全的文章 :迁移指南 、深入理解空安全 等,通过 迁移指南 我也将 fps_monitor 迁移空安全。但在对项目适配后,日常开发中我们该怎么使用?空安全究竟是什么?下面我们通过几个练习来快速上手 Flutter 空安全。


一、空安全解决了什么问题?

要想弄明白空安全是什么,我们先要知道空安全帮我们解决了什么?

先来看个例子

void main() {
  String stringNullException;
  print(stringNullException.length);
}

在适配空安全之前,这段代码在 在编译阶段不会有任何提示。但显然这是一段有问题的代码。在 Debug 模式下会抛出空异常,屏幕爆红提示。

I/flutter (31305): When the exception was thrown, this was the stack:
I/flutter (31305): #0      Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)

在 release 模式下,这个异常会让整个屏幕变成灰色。

这是一个典型的例子,stringNullException 在没有赋值的情况下是空的,但是却我们调用了 .length 方法,导致程序异常。

同样的代码在适配空安全之后,在编译期便给出了报错提示,开发者可以及时进行修复。

图片

image.png

所以简单的来说,空安全在代码编辑阶段帮助我们提前发现可能出现的空异常问题,但这并不意味着程序不会出现空异常


二、如何使用空安全?

那么空安全包含哪些内容,我们在日常开发的时候该如何使用?下面我们通过 Null safety codelab 中的几个练习来进行学习。

1、非空类型和可空类型

在空安全中,所有类型在默认情况下都是非空的。例如,你有一个 String 类型的变量,那么它应该总是包含一个字符串。

如果你想要一个 String 类型的变量接受任何字符串或者 null,通过在类型名称后添加一个问号(?)表示该变量可以为空。例如,一个类型为 String? 可以包含任何字符串,也可以为空。

练习 A:非空类型和可空类型

void main() {
  int a;
  a = null; // 提示错误,因为 int a 表示 a 不能为空
  print('a is $a.');
}

这段代码通过 int 声明了变量 a 是一个非空变量,在执行 a = null 的时候报错。可以修改为 int? 类型,允许 a 为空:

void main() {
  int? a; // 表示允许 a 为空
  a = null; 
  print('a is $a.');
}

练习 B:泛型的可空类型

void main() {
  List<String> aListOfStrings = ['one', 'two', 'three'];
  List<String> aNullableListOfStrings = [];
  // 报错提示,因为泛型 String 表示非 null
  List<String> aListOfNullableStrings = ['one', null, 'three']; 

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

在这个练习中,因为 aListOfNullableStrings 变量的类型是 List ,表示非空的 String 数组,但在后面创建过程中却提供了一个 null 元素,引起报错。因此可以将 null 改成其他字符串,或者在泛型中表示为可空的字符串。

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.');
}

2、空断言操作符(!)

如果确定某个 可为空的表达式 非空,可以使用空断言操作符 ! 使 Dart 将其视为非空。通过添加 ! 在表达式之后,可以将其赋值给一个非空变量。

练习 A:空断言

/// 这个方法的返回值可能为空
int? couldReturnNullButDoesnt() => -3;

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

  // couldBeNullButIsnt 变量虽然可为空,但是已经赋予初始值,因此不会报错
  int a = couldBeNullButIsnt;
  // 列表泛型中声明元素可为空,与 int b 类型不匹配报错
  int b = listThatCouldHoldNulls.first; // first item in the list
  // 上面声明这个方法可能返回空,而 int c 表示非空,所以报错
  int c = couldReturnNullButDoesnt().abs(); // absolute value

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

在这个练习中,方法 couldReturnNullButDoesnt 和数组 listThatCouldHoldNulls 都通过可空类型进行声明,但是后面的变量 b 和 c,都是通过非空类型来声明,因此报错。可以在表达式最后加上 ! 表示操作非空(你必须确认这个表达式是一定不会为空,否则仍然可能引起空指针异常)修改如下:

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.');
}

3、类型提升

Dart 的 流程分析 中已经扩展到考虑零值性。不可能为空的可空变量会被视为非空变量,这种行为称为类型提升

bool isEmptyList(Object object) {
  if (object is! List) return false;
  // 在空安全之前会报错,因为 Object 对象并不包含 isEmpty 方法
  // 在空安全后不报错,因为流程分析会根据上面的判断语句将 object 变量提升为 List 类型。
  return object.isEmpty; 
}

这段代码在空安全之前会报错,因为 object 变量是 Object 类型,并不包含 isEmpty 方法。

在空安全后不会报错,因为流程分析会根据上面的判断语句将 object 变量提升为 List 类型。

练习 A:明确地赋值

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);
  // 报错提示,text 变量可能为空
  print(text.length);
}

这段代码中我们使用 String? 声明了一个可空的变量 text,在后面直接使用了 text.length。Dart 会认为这是不安全的,因此报错提示。

但当我们去掉上面注释的代码后,将不会在报错。因为 Dart 对 text 赋值的地方判断后,认为 text 不会为空,将 text 提升为非空类型(String),不再报错。

练习 B:空检查

int getLength(String? str) {
  // 此处报错,因为 str 可能为空
  return str.length;
}

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

这个例子中,因为 str 可能为空,所以使用 str.length 会提示错误,通过类型提升我们可以这样修改:

int getLength(String? str) {
  // 判断 str 为空的场景 str 提升为非空类型
  if (str == null) return 0;
  return str.length;
}

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

提前判断 str 为空的场景,这样后面 str 的类型由 String?(可空)提升为 String(非空),不再报错。

3、late 关键字

有时变量(例如:类中的字段或顶级变量)应该是非空的,但不能立即给它们赋值。对于这种情况,使用 late 关键字。

当你把 late 放在变量声明的前面时,会告诉 Dart 以下信息:

  • 先不要给变量赋值。

  • 稍后将为它赋值

  • 你会在使用前对这个变量赋值。

  • 如果在给变量赋值之前读取该变量,则会抛出一个错误。

练习 A:使用 late

class Meal {
  // description 变量没有直接或者在构造函数中赋予初始值,报错
  String description;

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

这个例子中,Meal 类包含一个非空变量 description,但该变量却没有直接或者在构造函数中赋予初始值,因此报错。这种情况下,我们可以使用 late 关键字 表示这个变量是延迟声明:

class Meal {
  // late 声明不在报错
  late String description;
  void setDescription(String str) {
    description = str;
  }
}
void main() {
  final myMeal = Meal();
  myMeal.setDescription('Feijoada!');
  print(myMeal.description);
}

练习 B:循环引用下使用 late

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!');
}

通过添加 late 关键字解决报错。注意,我们不需要删除 final。late final 声明的变量表示:只需设置它们的值一次,然后它们就成为只读变量

class Team {
  late final Coach coach;
}

class Coach {
  late final Team team;
}

void main() {
  final myTeam = Team();
  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;
  print('All done!');
}

练习 C: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}!');
}

这个练习并不会报错,不过可以看看运行这段代码的输出结果:

Calling constructor...
In _computeValue...
Getting value...
The value is 3!

在打印完第一句 Calling constructor... 之后,生成 CachedValueProvider() 对象。生成过程会初始化它的变量 final _cache = _computeValue() 所以打印第二句话 In _computeValue...,再打印后续的语句。

当我们对 _cache 变量添加 late 关键字后,结果又如何?

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

class CachedValueProvider {
  // late 关键字,该变量不会在构造的时候初始化
  late final _cache = _computeValue();
  int get value => _cache;
}

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

日志如下:

Calling constructor...
Getting value...
In _computeValue...
The value is 3!

日志中In _computeValue... 的执行被延后了,其实就是 _cache 变量没有在构造的时候初始化,而是延迟到了使用的时候。


四、空安全并不意味没有空异常

这几个练习,也更加的反应了安全的作用:空安全在代码编辑阶段帮助我们提前发现可能出现的空异常问题。但要注意,这并不意味着不存在空异常。例如下面的例子

void main() {
  String? text;
  print(text);
  // 不会报错,因为使用 ! 断言 表示 text 变量不可能为空
  print(text!.length);
}

因为 text!.length 表示变量 text 不可能为空。但实际上 text 可能因为各种原因(例如,json 解析为 null)为空,导致程序异常。

上面 late 关键字的场景同样也会存在:

class Meal {
  // late 声明编辑阶段将不会报错
  late String description;
  void setDescription(String str) {
    description = str;
  }
}
void main() {
  final myMeal = Meal();
  // 先去读取这个未初始化变量,导致异常
  print(myMeal.description);
  myMeal.setDescription('Feijoada!');
}

我们在对 description 赋值之前提前读取,同样会导致程序异常。

所以还是那句话:空安全只是在代码编辑阶段帮助我们提前发现可能出现的空异常问题,但这并不意味着程序不会出现空异常。开发者仍需要对代码进行完善的边界判断,确保程序的健壮运行!

看到这儿给大家留个作业,如何在空安全下写工厂单例,欢迎在评论区留下你的答案,我会在下周公布答案~。

如果你还想了解更多关于空安全的文章,推荐:

  • 深入理解空安全

  • Null safety codelab

  • 健全的空安全


五、最后 感谢各位吴彦祖和彭于晏的点赞和关注

感谢 Alex 在空安全文档上的贡献。

图片

image.png

我近期也将翻译:Null safety codelab 欢迎关注。

如果你对 Flutter 其他内容感兴趣,推荐阅读往期精彩文章:

ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案
将在本月内进行开源,欢迎关注

Widget、Element、Render树究竟是如何形成的?

ListView的构建过程与性能问题分析

深度分析·不同版本中的 Flutter 生命周期差异

欢迎搜索公众号:进击的Flutter或者runflutter 里面整理收集了最详细的Flutter进阶与优化指南。关注我,探讨你的问题,获取我的最新文章~

热门文章

暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

GMX 命令分类列表

建模和计算操作命令&#xff1a; 1.1 . 创建拓扑与坐标文件 gmx editconf - 编辑模拟盒子以及写入子组(subgroups) gmx protonate - 结构质子化 gmx x2top - 根据坐标生成原始拓扑文件 gmx solvate - 体系溶剂化 gmx insert-molecules - 将分子插入已有空位 gmx genconf - 增加…
暂无图片
编程学习 ·

一文高效回顾研究生课程《数值分析》重点

数值分析这门课的本质就是用离散的已知点去估计整体&#xff0c;就是由黑盒子产生的结果去估计这个黑盒子。在数学里这个黑盒子就是一个函数嘛&#xff0c;这门课会介绍许多方法去利用离散点最大化地逼近这个函数&#xff0c;甚至它的导数、积分&#xff0c;甚至微分方程的解。…
暂无图片
编程学习 ·

在职阿里5年,一个28岁女软测工程师的心声

简单的先说一下&#xff0c;坐标杭州&#xff0c;14届本科毕业&#xff0c;算上年前在阿里巴巴的面试&#xff0c;一共有面试了有6家公司&#xff08;因为不想请假&#xff0c;因此只是每个晚上去其他公司面试&#xff0c;所以面试的公司比较少&#xff09; ​ 编辑切换为居中…
暂无图片
编程学习 ·

字符串左旋c语言

目录 题目&#xff1a; 解题思路&#xff1a; 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 总代码&#xff1a; 题目&#xff1a; 实现一个函数&#xff0c;可以左旋字符串中的k个字符。 例如&#xff1a; ABCD左旋一个字符得到BCDA ABCD左旋两个字符…
暂无图片
编程学习 ·

设计模式--观察者模式笔记

模式的定义与特点 观察者&#xff08;Observer&#xff09;模式的定义&#xff1a;指多个对象间存在一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xf…
暂无图片
编程学习 ·

睡觉突然身体动不了,什么是睡眠痽痪症

很多朋友可能有这样的体验&#xff0c;睡觉过程中突然意识清醒&#xff0c;身体却动弹不了。这时候感觉非常恐怖&#xff0c;希望旁边有一个人推自己一下。阳光以前也经常会碰到这样的情况&#xff0c;一年有一百多次&#xff0c;那时候很害怕晚上到来&#xff0c;睡觉了就会出…
暂无图片
编程学习 ·

深入理解C++智能指针——浅析MSVC源码

文章目录unique_ptrshared_ptr 与 weak_ptrstd::bad_weak_ptr 异常std::enable_shared_from_thisunique_ptr unique_ptr 是一个只移型别&#xff08;move-only type&#xff0c;只移型别还有std::mutex等&#xff09;。 结合一下工厂模式&#xff0c;看看其基本用法&#xff…
暂无图片
编程学习 ·

@TableField(exist = false)

TableField(exist false) //申明此字段不在数据库存在&#xff0c;但代码中需要用到它&#xff0c;通知Mybatis-plus在做写库操作是忽略它。,.
暂无图片
编程学习 ·

Java Web day15

第十二章文件上传和下载 一、如何实现文件上传 要实现Web开发中的文件上传功能&#xff0c;通常需要完成两步操作&#xff1a;一.是在Web页面中添加上传输入项&#xff1b;二是在Servlet中读取上传文件的数据&#xff0c;并保存到本地硬盘中。 需要使用一个Apache组织提供一个…
暂无图片
编程学习 ·

【51nod 2478】【单调栈】【前缀和】小b接水

小b接水题目解题思路Code51nod 2478 小b接水 题目 输入样例 12 0 1 0 2 1 0 1 3 2 1 2 1输出样例 6解题思路 可以发现最后能拦住水的都是向两边递减高度&#xff08;&#xff1f;&#xff09; 不管两个高积木之间的的积木是怎样乱七八糟的高度&#xff0c;最后能用来装水的…
暂无图片
编程学习 ·

花了大半天写了一个UVC扩展单元调试工具

基于DIRECTSHOW 实现的&#xff0c;用的是MFC VS2019. 详见&#xff1a;http://www.usbzh.com/article/detail-761.html 获取方法 加QQ群:952873936&#xff0c;然后在群文件\USB调试工具&测试软件\UVCXU-V1.0(UVC扩展单元调试工具-USB中文网官方版).exe USB中文网 USB中文…
暂无图片
编程学习 ·

贪心(一):区间问题、Huffman树

区间问题 例题一&#xff1a;区间选点 给定 N 个闭区间 [ai,bi]请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 N&#xff0c;表示区间数。 接下来 …
暂无图片
编程学习 ·

C语言练习实例——费氏数列

目录 题目 解法 输出结果 题目 Fibonacci为1200年代的欧洲数学家&#xff0c;在他的着作中曾经提到&#xff1a;「若有一只免子每个月生一只小免子&#xff0c;一个月后小免子也开始生产。起初只有一只免子&#xff0c;一个月后就有两只免子&#xff0c;二个月后有三只免子…
暂无图片
编程学习 ·

Android开发(2): Android 资源

个人笔记整理 Android 资源 Android中的资源&#xff0c;一般分为两类&#xff1a; 系统内置资源&#xff1a;Android SDK中所提供的已经定义好的资源&#xff0c;用户可以直接拿来使用。 用户自定义资源&#xff1a;用户自己定义或引入的&#xff0c;只适用于当前应用的资源…
暂无图片
编程学习 ·

零基础如何在短时间内拿到算法offer

​算法工程师是利用算法处理事物的职业 算法&#xff08;Algorithm&#xff09;是一系列解决问题的清晰指令&#xff0c;也就是说&#xff0c;能够对一定规范的输入&#xff0c;在有限时间内获得所要求的输出。 如果一个算法有缺陷&#xff0c;或不适合于某个问题&#xff0c;执…
暂无图片
编程学习 ·

人工智能:知识图谱实战总结

人工智能python&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习人工智能&#xff1a;知识图谱实战前言一、实体建模工具Protegepython&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习 人工智能&#xff1a;知识图…
暂无图片
编程学习 ·

【无标题】

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…