导语

在 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 表示非 nullList<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 valueprint('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 listint c = couldReturnNullButDoesnt()!.abs(); // absolute valueprint('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进阶与优化指南。关注我,探讨你的问题,获取我的最新文章~

快速上手 Flutter 空安全相关推荐

  1. 『转载』Debussy快速上手(Verdi相似)

    『转载』Debussy快速上手(Verdi相似) Debussy 是NOVAS Software, Inc(思源科技)发展的HDL Debug & Analysis tool,这套软体主要不是 ...

  2. WijmoJS 2019V1正式发布:全新的在线 Demo 系统,助您快速上手,开发无忧

    2019独角兽企业重金招聘Python工程师标准>>> 下载WijmoJS 2019 v1 WijmoJS是为企业应用程序开发而推出的一系列包含HTML5和JavaScript的开发 ...

  3. react 快速上手开发_React中测试驱动开发的快速指南

    react 快速上手开发 by Michał Baranowski 通过MichałBaranowski React中测试驱动开发的快速指南 (A quick guide to test-driven ...

  4. Tensorflow 10分钟快速上手

    Tensorflow 快速上手 系统版本 : Ubuntu 16.04LTS Python版本 : 3.6.1 Tensorflow 版本 : 1.0.1 本文依据教程 TensorFlow Tuto ...

  5. smarty半小时快速上手入门教程

    本文讲述了smarty快速上手入门的方法,可以让读者在半小时内快速掌握smarty的用法.分享给大家供大家参考.具体实现方法如下: 一.smarty的程序设计部分: 在smarty的模板设计部分我简单 ...

  6. centos6.5 yum安装mysql_CentOS 6.5使用yum安装MySQL快速上手必备

    CentOS 6.5使用yum安装MySQL快速上手必备 第1步.yum安装mysql [root@stonex ~]#  yum -y install mysql-server 安装结果: Inst ...

  7. TensorFlow 2.0 快速上手教程与手写数字识别例子讲解

    文章目录 TensorFlow 基础 自动求导机制 参数优化 TensorFlow 模型建立.训练与评估 通用模型的类结构 多层感知机手写数字识别 Keras Pipeline * TensorFlo ...

  8. mysql快速上手3

    上一章给大家说的是数据库的视图,存储过程等等操作,这章主要讲索引,以及索引注意事项,如果想看前面的文章,url如下: mysql快速上手1 mysql快速上手2 索引简介 索引是对数据库表中一个或多个 ...

  9. raptor累乘流程图_Markdown快速上手指南

    Markdown快速上手指南 1.Markdown介绍 markdown可以实现快速html文档编辑,格式优没,并且不需要使用html元素. markdown采用普通文本的形式,例如读书笔记等易于使用 ...

最新文章

  1. springboot html引入js_SpringBoot-05-web开发
  2. 2020年十大数据中心行业趋势
  3. 2018年高教社杯全国大学生数学建模竞赛题目问题B 智能RGV的动态调度策略
  4. RocketMQ部署安装(非Docker安装)
  5. android studio ignore 模板,android studio git ignore
  6. J2EE中EL表达式
  7. hashmap put过程_HashMap为什么线程不安全?
  8. IA64与x64的区别
  9. three20 如何将three20中的demo添加到自己的应用程序中。
  10. LVS-三种负载均衡方式比较
  11. 编译器vc6 新手使用教程(C、C++)
  12. radius服务器认证系统,TekRadius(RADIUS服务器)
  13. nginx和ftp搭建图片服务器
  14. WPS工具栏都是灰色不能编辑解决方法分享
  15. DOM是什么?有什么用处?js与DOM啥关系?
  16. 三部曲简史mobi_尤瓦尔简史三部曲:人类简史+未来简史+今日简史
  17. KepServer如何和欧姆龙NJ系列通讯并进行字符串读取
  18. LeetCode135. 分发糖果(贪心算法)
  19. caffe学习(4)数据层
  20. 费城交响乐团将于5月16日至28日开启2019年中国巡演之旅

热门文章

  1. 科罗拉多州立大学计算机科学,科罗拉多州立大学本科什么专业好
  2. shell之read用法
  3. 200万年薪,西交大2位计算机博士入选华为天才少年
  4. OpenNI + OpenCV
  5. 计算机病毒在我国的发展情况,计算机病毒检测技术的现状与发展
  6. iOS 15 内置原生壁纸下载
  7. ctfshow摆烂杯
  8. 集成平台、大数据平台、数据治理平台,医院信息科应该怎么选?
  9. bootstrapNPM淘宝代理镜像
  10. 常见的五大数据分析模型