变量与Null safety
什么是变量
计算机编程语言里的变量(Variable)与数学公式里的变量类似。请看下面这段Dart代码:
// ch0102-1
void main() {
var x = 3; // 1
var (y, z) = (4, 5); // 2
print('x = $x, y = $y, z = $z'); // 3 Output: x = 3, y = 4, z = 5
print('x + y + z = ${x + y + z}'); // 4 Output: x + y + z = 12
}
这段简短的代码只有一个函数,那就是main
,整个程序的入口。代码中的 //
是注释,是为了帮助人们阅读代码,Dart编译器会忽略它。
- 使用
var
关键字,声明变量x,并给x
赋值为 3,Dart编译器自动推断其类型为int
(integer, 整数); - 同时声明两个变量
x
和y
,分别赋值为4和5,这里使用了record的模式匹配(第3章内容,这里看不懂也没关系); - 分别将
x
,y
,z
的值打印(显示)到控制台(标准输出设备 stdout); - 计算
x + y + z
并打印。
第1句也可以写成 int x = 3;
也等同于
int x;
x = 3;
第2句也可以写成
var y= 4, z = 5;
第3、4两句中的 $x
, $y
, $z
以及 ${x + y + z}
为 字符串插值,非常直观和方便。
var x
声明了一个变量,它的标识符为x
; 标识符,通俗地说就是给变量、函数、类、方法等起个名字,方便使用,就像我们给宠物取个名,方便呼唤。Dart语言的标识符可以是下列字符的组合:
- 字母
- 数字
_
和$
但不能以数字开头。这些都是合法的标识符: practiceMakesPerfect
, _imPrivate
, _$GeneratedCode
, $happy365
。
var
修饰的变量 ,可以后续更改,比如
// ch0102-3
void main() {
var x = 3;
x = 30;
print('x = $x'); // Output: x = 30
}
变量除了可以表示数字,还可以表示文本(字符串)等. 例如
// ch0102-4
void main(){
var name = 'Dart';
print('Hello, $name!'); // Output: Hello, Dart!
}
这里的数字、字符串等,就是所谓的数据类型(本章第4节内容,这里不再展开)。
优先使用 var
声明变量
对于 var x = 3;
也可以写成 int x = 3
,你可能有个疑问,哪种情况下用哪种方式呢?官方的建议是,多数情况下,我们应该使用 var (而非实际类型) 声明变量,这样做除了可以使代码更短之外,还能提高代码的可读性。
如果我们仅仅声明变量(不赋值),应该写出实际的类型,因为这时候Dart无法推断出变量的数据类型,如
int x;
如果我们这样写
var x;
Dart编译器就认为 x 的类型为 dynamic
,这种类型可以表示任何数据,通常情况下我们应该避免这样做,因为Dart的静态类型检查对dynamic
无效(可能暗藏bug)。
使用 late
延迟初始化
有时候我们希望延迟变量的初始化(特别在Flutter的StatefulWidget中),这时就用到 late
关键字, 例如
// ch0102-5
void main() {
late String action = 'go camping';
var isReady = true; // it may take some time to prepare
if (isReady) {
print("Let's $action!"); // Output: Let's go camping!
}
}
这里先了解下,第4章会有更多关于 late
的讨论。
Null safety
NNBD
从Dart 2.10 开始,变量默认非 null
(non-nullable by defaullt, NNBD);null
表示空值。 比如下面这段代码无法编译
// ch0102-6
void main() {
int x;
print('x = $x');
// Error: Non-nullable variable 'x' must be assigned before it can be used.
// print('x = $x');
// ^
}
当我们尝试编译上面这段代码时,出现编译错误,告诉我们在使用变量x
之前必须初始化它。
如果要使用null
, 可以像下面这样:
// ch0102-7
void main() {
int? x;
print('x = $x'); // Output: x = null
}
这里的 int? x
表示变量x
的初始值为null
, x
是一个nullable变量。
注:
熟悉Java的朋友想必对NullPointerException
(NPE) 并不陌生,这个Exception是一个运行时异常;为了提高代码的健壮性,经常要对方法的入参进行非空检查,否则就很容易遭遇NPE(这个问题有时难以排查,尤其在大型软件项目里)。例如下面这段Java代码
void doSomething(String str) {
if (str == null){
throw new IllegalArgumentException("doSomething: str cannot be null");
}
// Do something
}
我想这便是Dart NNBD的原因。
访问nullable变量
有几个nullable变量相关的访问/赋值,有必要介绍一下,你以后再看到下面这些符号就不会感到陌生了。
??
、 ??=
、 !
// ch0102-8
void main() {
int? x;
var y = x ?? 10; // 1
var z = y ?? 20; // 1a
print('y = $y, z = $z'); // Output: y = 10, z = 10
int? y2;
y ??= 100; // 2
y2 ??= 100; // 2a
print('y = $y, y2 = $y2'); // Output: y = 10, y2 = 100
var a = y!; // 3
print('a = $a'); // Output: a = 10
var b = x!; // 3a
print('b = $b');
// Unhandled exception:
// Null check operator used on a null value
}
- 如果
x
为null
,y
赋值为10, 否则将x
的值赋给y
; - 如果
y
为null
,y
赋值为100,否则啥也不做; - 断言
y
一定非空(null
),并将y
的值赋给a
;如果y
为null
,在程序运行时会出现一个Null check异常(见3a)。
请对比 1和1a, 2和2a,以及3和3a的结果。
?.
、?[]
// ch0102-9
void main() {
var abc = 'ABCD';
String? def;
var first = abc?[0]; // 1
var second = def?[0]; // 1a
print('first = $first, second = $second'); // Output: first = A, second = null
var len1 = abc?.length; // 2
var len2 = def?.length; // 2a
print('len1 = $len1, len2 = $len2'); // Output: len1 = 4, len2 = null
var len3 = def?.length ?? 0; // 3
print('len3 = $len3'); // Output: len3 = 0
}
- 如果
abc
为null
,first
为null
, 否则将abc[0]
的值赋给first
; - 如果
abc
为null
,len1
为null
, 否则将abc.length
的值赋给len1
; - 这是一个综合的例子,同时使用了
?.
与??
符号。
注:本例中的 abc[0]
返回第0个(下标从0开始)unicode字符,abc.length
返回abc
的长度。
...?
// ch0102-a
void main() {
var arr = [1, 2, 3];
var arr2 = [...arr, 4, 5]; // 1
print('arr2 = $arr2'); // Output: arr2 = [1, 2, 3, 4, 5]
var arr3 = [...?arr, 6, 7]; // 1a
print('arr3 = $arr3'); // Output: arr3 = [1, 2, 3, 6, 7]
List<int>? arr4;
var arr5 = [...?arr4, 8, 9]; // 2
print('arr5 = $arr5'); // Output: arr5 = [8, 9]
List<int>? arr6 = []; // 3
var arr7 = [...arr6, -1, -2]; // 3a
print('arr7 = $arr7'); // Output: arr7 = [-1, -2]
}
arr
是一个列表(数组),...arr
将展开arr
, 此行代码将arr
以及 4, 5 一起组成一个新的列表,并将其赋值给arr2
;- 本行代码 与 1 类似, 区别在于,这里必须使用
...?arr4
执行展开操作,否则无法编译通过; arr6
跟arr4
一样,是一个nullable变量,但其初始值为一个空的列表(不是null
),就可以用...
对其进行展开,可见Dart编译器非常智能。