本节目标

  • 空安全意味着什么
  • 如何迁移代码
  • 如何禁用空安全
  • 代码规范示例

视频

https://www.bilibili.com/video/bv1g5411c7hF

代码

https://github.com/ducafecat/getx_quick_start

参考

正文

空安全意味着什么

  • 默认不可空
1
String title = 'ducafecat';
  • type? 操作符
1
String? title = null;
  • value! 操作符
1
2
String? title = 'ducafecat';
String newTitle = title!;
  • value? 操作符
1
2
String? title = 'ducafecat';
bool isEmpty = title?.isEmpty();
  • value?? 操作符
1
2
String? title = 'ducafecat';
String newTitle = title ?? 'cat';
  • late 会在运行时检查。所以请您仅在确定它被使用前一定会被初始化的情况下使用
1
2
late String? title;
title = 'ducafecat';
  • List、泛型
类型 集合是否可空 数据项是否可空
List no no
List? yes no
List<String?> no yes
List<String?>? yes yes
  • Map
类型 集合是否可空 数据项是否可空
Map<String, int> no no*
Map<String, int>? yes no*
Map<String, int?> no yes
Map<String, int?>? yes yes

* 可能返回空

1
2
3
4
5
6
7
8
// 有可能返回 null
int value = <String, int>{'one': 1}['one']; // ERROR

// 需要加上 type?
int? value = <String, int>{'one': 1}['one']; // OK

// 或者 value!
int value = <String, int>{'one': 1}['one']!; // OK

带来的好处

  • 代码更健康
  • 用户体验好
  • 运行更快
  • 编译文件更小

开启和迁移

  • pubspec.yaml
1
2
environment:
sdk: ">=2.12.0 <3.0.0"
  • 迁移顺序

我们强烈建议您按顺序迁移代码,先迁移依赖关系中的处于最末端的依赖。例如,如果 C 依赖了 B,B 依赖了 A,那么应该按照 A -> B -> C 的顺序进行迁移。

  • 检查依赖项目
1
2
3
4
5
# Dart 版本是否为 2.12 或更高
> dart --version

# 依赖包的迁移状态
> dart pub outdated --mode=null-safety
  • 升级依赖
1
2
3
4
5
# 该命令会更改您的 pubspec.yaml 文件
> dart pub upgrade --null-safety

# 升级包
> dart pub upgrade
  • 迁移工具
1
> dart migrate
  • 分析
1
> dart analyze

禁用空安全

  • cli 命令
1
2
> dart --no-sound-null-safety run
> flutter run --no-sound-null-safety
  • .vscode/launch.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "getx_quick_start",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["--no-sound-null-safety"]
}
]
}

范例、规范

https://dart.cn/null-safety/understanding-null-safety

  • 明确处理空状态
1
2
3
4
5
6
7
makeCoffee(String coffee, [String? dairy]) {
if (dairy != null) {
print('$coffee with $dairy');
} else {
print('Black $coffee');
}
}
  • 顶层变量和静态字段必须包含一个初始化方法。由于它们能在程序里的任何位置被访问到,编译器无法保证它们在被使用前已被赋值。唯一保险的选项是要求其本身包含初始化表达式,以确保产生匹配的类型的值。
1
2
3
4
5
int topLevel = 0;

class SomeClass {
static int staticField = 0;
}
  • 实例的字段也必须在声明时包含初始化方法,可以为常见初始化形式,也可以在实例的构造方法中进行初始化。
1
2
3
4
5
6
7
8
class SomeClass {
int atDeclaration = 0;
int initializingFormal;
int initializationList;

SomeClass(this.initializingFormal)
: initializationList = 0;
}
  • 局部变量的灵活度最高。一个非空的变量 不一定需要 一个初始化方法。
1
2
3
4
5
6
7
8
9
10
11
int tracingFibonacci(int n) {
int result;
if (n < 2) {
result = n;
} else {
result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
}

print(result);
return result;
}
  • 流程分析,在这里 Dart 将 object 的类型从它声明的 Object 提升到了 List。在空安全引入以前,下面的程序无法运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// With (or without) null safety:
bool isEmptyList(Object object) {
if (object is List) {
return object.isEmpty; // <-- OK!
} else {
return false;
}
}

->

// Without null safety:
bool isEmptyList(Object object) {
if (object is! List) return false;
return object.isEmpty;
}
  • 绝对的赋值分析
1
2
3
4
5
6
7
8
9
10
11
int tracingFibonacci(int n) {
final int result;
if (n < 2) {
result = n;
} else {
result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
}

print(result);
return result;
}
  • 无用代码的警告
1
2
3
4
5
6
String checkList(List list) {
if (list?.isEmpty) {
return 'Got nothing';
}
return 'Got something';
}
  • 懒加载的变量, late 修饰符是“在运行时而非编译时对变量进行约束”。这就让 late 这个词语约等于 何时 执行对变量的强制约束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Using null safety:
class Coffee {
String? _temperature;

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

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

->

// Using null safety:
class Coffee {
late String _temperature;

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

String serve() => _temperature + ' coffee';
}
  • latefinal 结合使用,与普通的 final 字段不同,您不需要在声明或构造时就将其初始化。您可以稍后在运行中的某个地方加载它。但是您只能对其进行 一次 赋值,并且它在运行时会进行校验。
1
2
3
4
5
6
7
8
9
// Using null safety:
class Coffee {
late final String _temperature;

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

String serve() => _temperature + ' coffee';
}
  • 毕传参数,这里的所有参数都必须通过命名来传递。参数 ac 是可选的,可以省略。参数 bd 是必需的,调用时必须传递。在这里请注意,是否必需和是否可空无关。
1
2
// Using null safety:
function({int? a, required int? b, int? c, required int? d}) {}
  • 抽象字段
1
2
3
4
5
6
7
8
9
10
abstract class Cup {
Beverage get contents;
set contents(Beverage);
}

->

abstract class Cup {
abstract Beverage contents;
}
  • 一些赋值计算可以移动到静态的初始化中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Initalized without values
ListQueue _context;
Float32List _buffer;
dynamic _readObject;

Vec2D(Map<String, dynamic> object) {
_buffer = Float32List.fromList([0.0, 0.0]);
_readObject = object['container'];
_context = ListQueue<dynamic>();
}

->

// Initalized with values
final ListQueue _context = ListQueue<dynamic>();
final Float32List _buffer = Float32List.fromList([0.0, 0.0]);
final dynamic _readObject;

Vec2D(Map<String, dynamic> object) : _readObject = object['container'];
  • 可能返回 null 的工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
factory StreamReader(dynamic data) {
StreamReader reader;
if (data is ByteData) {
reader = BlockReader(data);
} else if (data is Map) {
reader = JSONBlockReader(data);
}
return reader;
}

->

factory StreamReader(dynamic data) {
if (data is ByteData) {
// Move the readIndex forward for the binary reader.
return BlockReader(data);
} else if (data is Map) {
return JSONBlockReader(data);
} else {
throw ArgumentError('Unexpected type for data');
}
}

© 猫哥

https://ducafecat.tech

https://ducafecat.gitee.io