变量与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编译器会忽略它。

  1. 使用var关键字,声明变量x,并给x赋值为 3,Dart编译器自动推断其类型为 int (integer, 整数);
  2. 同时声明两个变量xy,分别赋值为4和5,这里使用了record的模式匹配(第3章内容,这里看不懂也没关系);
  3. 分别将xyz的值打印(显示)到控制台(标准输出设备 stdout);
  4. 计算 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
}
  1. 如果 xnully赋值为10, 否则将x的值赋给y
  2. 如果 ynully赋值为100,否则啥也不做;
  3. 断言y一定非空(null),并将y的值赋给a;如果ynull,在程序运行时会出现一个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
}
  1. 如果abcnullfirstnull, 否则将abc[0]的值赋给first
  2. 如果abcnulllen1null, 否则将abc.length的值赋给len1
  3. 这是一个综合的例子,同时使用了 ?.?? 符号。

注:本例中的 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]
}
  1. arr是一个列表(数组), ...arr将展开arr, 此行代码将arr以及 4, 5 一起组成一个新的列表,并将其赋值给arr2;
  2. 本行代码 与 1 类似, 区别在于,这里必须使用 ...?arr4 执行展开操作,否则无法编译通过;
  3. arr6arr4一样,是一个nullable变量,但其初始值为一个空的列表(不是null),就可以用...对其进行展开,可见Dart编译器非常智能。

参考资料