Dart: Constant and Final

 12th December 2020 at 11:52pm

常量与非常量是针对编译期而言。区别是:

  • 常量:用 const 指定,声明变量时就必须指定值,无法再被重新赋值,编译器会展开它的值
  • 非常量:
    • var:编译器无法在编译期知道它的值
    • final:跟 var 相同,除了它只能被赋值一次,无法再被重新赋值

对于简单类型的字面值

对于简单的字面值,代码写起来是很类似的:

const i1 = 3;
const int i2 = 4;

final i3;
i3 = 5;
i3 = 6;    // 错误:i3 只能被设置一次

对于类实例

对于类实例,还是有一些区别。const 要求类实现 const 构造函数(因为它在编译期要保证值无法改变)。final 没有这种要求:

class Vector {
  int x, y;
  Vector(this.x, this.y);
}

const v1 = Vector(1, 2);    // 错误:Vector 类的构造函数不是 const 构造函数
final v2 = Vector(1, 2);    // 正确:final 不要求 const 构造函数
v2 = Vector(2, 3);          // 错误:final 限制了变量无法被重新赋值

如果你要使用 const 构造函数,那么类中的所有属性必须是 final 的(不能再被赋值):

class Vector {
  final int x, y;
  const Vector(this.x, this.y);
}

const v1 = Vector(1, 2);    // 正确:Vector 类有了 const 构造函数及 final 属性

但 Dart 在设计 const 时有点奇怪,const 不仅能用来修饰一个变量(如上面的 v1),它还可以修饰对象的创建过程。事实上:

const v1 = Vector(1, 2);

// 等价于
const v1 = const Vector(1, 2);
// 只是这种 const 编译器可以推断出来,所以可以省略掉

// 你也可以
var v2 = const Vector(1, 3);
// v2 非 const 仅表示它可以被重新赋值
v2 = const Vector(2, 3);
// 但它的值仍然是一个 const 对象,仍然无法修改它的属性
v2.x = 3;         // 错误:Vector.x 是 final 的

但是对象的创建过程加不加 const 都是没有意义的,因为 const 构建函数决定了创建出来的对象必然是 const 的。

如上所示,一旦构建函数是 const 的,你就只能初始化出常量的 Vector 实例。但是实际上经常需要用到非常量的实例。因此实践上最好上写两个 Vector 类:

class Vector {
  int x, y;
  Vector(this.x, this.y);
}

class ImmutableVector {
  final int x, y;
  const Vector(this.x, this.y);
}

const 从机制上保证了对象各属性值是编译期确定好的,所以你无法再修改 v1.x 的值;但是 final 没有这种保证:

class Vector {
  int x, y;
  Vector(this.x, this.y);
}

final v2 = Vector(1, 2);
v2.x = 3;        // 正确:Vector.x 不是 final 的,可以被修改

无法被修改的集合

Dart 为了实现不可变的集合,复用了 const 关键字,产生了一些不太直观的代码。比如一个无法被修改的 List,可以这样创建:

var foo = const [];
final bar = const [];
const baz = []; // 等同于 `const []`

// 下面几种操作都会在运行时报错:
// Unsupported operation: Cannot add to an unmodifiable list
bar.add(1);
foo.add(1);
baz.add(1);

同样的,Set 和 Map 也有类似的方法:

var m = const {};

// 运行时报错:
// Unsupported operation: Cannot set value in unmodifiable Map
m['a'] = 1;
var s = const <dynamic>{};

// 运行时报错:
// Unsupported operation: Cannot change unmodifiable set
s.add('a');

Dart 在这块的设计不够优雅。const 关键字被滥用了,它可以:

  • 修饰一个变量,表示它是编译期确定的常量
  • 修饰一个 const object 的创建过程,而且并没有什么用
  • 作为不可修改集合(unmodifiable collection)的字面值

这很容易引起混淆。很多人看到 const [1, 2] 这种语法应该会一头雾水。对比之下 Python 的做法则没有引入新的语法机制:

  • 可变列表用 [1, 2],不可变列表(tuple,元组)用 (1, 2)
  • 可变集合用 set({1, 2}) 或者字面值 {1, 2},不可变集合用 frozenset({1, 2})