跳到主要内容

Variables(变量)

变量用于给数据起名字,并在程序中保存或引用数据。Dart 是静态类型语言:每个变量都有确定的类型,编译器会在运行前检查大部分类型错误。

学习 Dart 变量时,重点需要掌握以下问题:

  • 如何使用显式类型或 var 声明变量?
  • 变量能不能保存 null
  • Object?dynamic 有什么区别?
  • late 什么时候使用?
  • finalconst 分别限制了什么?

声明变量

Dart 可以直接写出变量类型,也可以使用 var 让编译器根据初始值推断类型。

void main() {
String language = 'Dart';
int releaseYear = 2011;
var version = 3.12;

print('$language was released in $releaseYear.');
print('Current example version: $version');
}

上例中:

  • language 的类型被显式声明为 String
  • releaseYear 的类型被显式声明为 int
  • version 根据初始值 3.12 被推断为 double

显式类型和 var 都不会让变量失去类型。下面的赋值无法通过编译:

void main() {
var count = 1; // 推断为 int
count = 'one'; // 编译错误:String 不能赋值给 int
}

当类型从右侧初始值一眼就能看出时,通常可以使用 var;当类型信息不明显或需要强调接口时,可以显式写出类型。

TypeScript 对比:Dart 的 var 更接近 TypeScript 中带类型推断的 let,不是 JavaScript 中与 let 相对的旧式 var。Dart 不通过 letvar 区分作用域规则。

let version = 3.12 // 推断为 number
version = '3.12' // 类型检查错误

变量的类型不会随赋值改变

变量的静态类型在声明后就已经确定。即使后续赋入的是某个子类型的对象,变量本身的静态类型也不会改变。

void main() {
num score = 90;
score = 95.5;

print(score);
}

numintdouble 的共同父类型,因此 score 可以先保存整数,再保存小数。但它的静态类型始终是 num,并不是先变成 int,再变成 double

空安全与可空变量

Dart 默认启用健全的空安全(sound null safety)。类型末尾没有 ? 的变量不能保存 null;带有 ? 的类型表示该变量可以保存对应类型的值,也可以保存 null

void main() {
String title = 'Dart variables';
String? subtitle;

print(title);
print(subtitle); // null

subtitle = 'Null safety';
print(subtitle); // Null safety
}

这里的 StringString? 是不同的类型:

类型可以保存的值
String字符串,不能是 null
String?字符串或 null

不可空的局部变量可以先声明、稍后赋值,但编译器必须能够确认它在读取前已经完成初始化。

void main() {
int result;

if (DateTime.now().hour < 12) {
result = 1;
} else {
result = 2;
}

print(result);
}

如果某条执行路径可能在赋值前读取 result,代码就无法通过编译。这种检查称为确定赋值分析(definite assignment analysis)。

TypeScript 对比:两种语言都可以用联合空值的方式理解可空类型。Dart 使用 String?,TypeScript 通常写成 string | null。Dart 的 ? 是其空安全类型系统的一部分。

let subtitle: string | null = null
subtitle = 'Null safety'

ObjectObject?dynamic

当变量可能保存多种不同类型的值时,可以使用 ObjectObject?dynamic,但它们提供的类型安全程度不同。

ObjectObject?

所有非空 Dart 对象都可以赋值给 Object。如果还需要允许 null,则使用 Object?

void main() {
Object value = 'Dart';
value = 3;

Object? nullableValue = null;
nullableValue = true;

print(value);
print(nullableValue);
}

编译器只允许通过 ObjectObject? 变量访问该静态类型已知的成员。使用具体类型的成员前,需要先进行类型检查:

void main() {
Object value = 'Dart';

if (value is String) {
print(value.toUpperCase());
}
}

if 分支内,编译器确认 value is String 后,会将 value 提升(promote)为 String,因此可以安全调用 toUpperCase()

dynamic

dynamic 会推迟很多静态类型检查。编译器允许调用它的任意成员,但成员不存在时会在运行时失败。

void main() {
dynamic value = 'Dart';
print(value.toUpperCase());

value = 3;
print(value + 1);
}

下面的代码可以通过静态检查,但运行时会因为 int 没有 toUpperCase() 方法而抛出异常:

void main() {
dynamic value = 3;
print(value.toUpperCase()); // 运行时错误
}

因此,在只需要“能够保存任意类型的值”时,优先考虑 Object?;只有确实需要绕过静态检查,例如处理缺少类型信息的动态数据时,才使用 dynamic

声明类型是否接受 null成员访问是否经过静态检查
Object
Object?
dynamic很多检查被推迟到运行时

TypeScript 对比:可以把 Dart 的 Object? 粗略类比为 TypeScript 的 unknown,使用具体能力前都需要缩小类型;dynamic 则更接近 any。两组类型并不完全等价,但在选择安全边界时具有相似思路。

延迟初始化:late

late 表示一个非空变量会在声明之后初始化。它适用于初始化时机较晚,但业务逻辑可以保证变量在读取前一定有值的场景。

late String greeting;

void main() {
greeting = 'Hello, Dart!';
print(greeting);
}

编译器不再要求在声明处提供初始值,但责任转移给了开发者。如果变量在赋值前被读取,会抛出 LateInitializationError

late String greeting;

void main() {
print(greeting); // 运行时抛出 LateInitializationError
}

late 还可以让初始化表达式延迟到变量第一次被读取时执行:

String loadConfiguration() {
print('Loading configuration...');
return 'production';
}

late String environment = loadConfiguration();

void main() {
print('Program started');
print(environment);
}

输出顺序为:

Program started
Loading configuration...
production

如果 environment 从未被读取,loadConfiguration() 就不会执行。

不要仅仅为了避开可空类型而滥用 late。如果“尚未设置”本身是合法状态,应使用可空类型;如果读取前必须完成初始化,并且初始化时机确实较晚,才考虑 late

TypeScript 对比:TypeScript 类字段后的确定赋值断言 ! 可以告诉编译器该字段稍后会被初始化,但它本身不提供 Dart late 的运行时初始化检查,也不等同于惰性初始化。

只赋值一次:final

final 变量只能被赋值一次。它的值可以在程序运行时计算。

void main() {
final startedAt = DateTime.now();
final String environment = readEnvironment();

print(startedAt);
print(environment);
}

String readEnvironment() => 'development';

下面的第二次赋值无法通过编译:

void main() {
final language = 'Dart';
language = 'TypeScript'; // 编译错误:final 变量不能再次赋值
}

final 限制的是变量不能再次指向另一个对象,不代表对象本身不可修改。

void main() {
final topics = ['variables'];
topics.add('functions');

print(topics); // [variables, functions]
}

这里不能写 topics = ['classes'],但可以修改 topics 所引用的列表内容。

当值需要稍后才能确定,同时又只允许赋值一次时,可以组合使用 late final

late final String token;

void main() {
token = 'generated-at-runtime';
print(token);

// token = 'another-token'; // 编译错误:不能再次赋值
}

TypeScript 对比:Dart 的 final 在“变量绑定不能重新赋值”这一点上类似 TypeScript 的 const。两者都不自动保证所引用对象的内部状态不可变。

编译时常量:const

const 表示编译时常量。它的值必须能够在编译阶段确定,并且变量也不能再次赋值。

void main() {
const maxAttempts = 3;
const appName = 'Dart Notes';
const doubledAttempts = maxAttempts * 2;

print('$appName: $doubledAttempts');
}

运行时才能获得的值不能赋给 const

void main() {
const startedAt = DateTime.now(); // 编译错误:不是编译时常量
}

但它可以赋给 final

void main() {
final startedAt = DateTime.now();
print(startedAt);
}

const 集合本身不可修改:

void main() {
const topics = ['variables', 'functions'];
print(topics);

// topics.add('classes'); // 运行时会抛出不支持修改的错误
}

在常量上下文中,集合元素和对象构造也必须是编译时常量。Dart 还会对相同的常量对象进行规范化(canonicalization),让等价的常量表达式复用同一个实例。

void main() {
const first = ['Dart'];
const second = ['Dart'];

print(identical(first, second)); // true
}

finalconst 的选择

对比项finalconst
赋值次数只能一次只能一次
值的确定时间可以在运行时必须在编译时
引用的集合是否可修改可以不可以
常见用途请求结果、当前时间、运行时配置固定数字、固定文本、常量集合

选择时可以记住一句话:运行时确定但不再改变,用 final;编译时就能确定,用 const

TypeScript 对比:TypeScript 的 const 只限制变量绑定不能重新赋值,更接近 Dart 的 final。Dart 的 const 还要求值是编译时常量,并可用于创建不可变的常量对象,因此没有完全对应的 TypeScript 声明关键字。

通配符变量 _

在支持通配符变量的 Dart 版本中,非顶层作用域里的 _ 表示“这里需要一个变量位置,但不会使用这个值”。它不会创建可读取的变量,并且同一作用域可以出现多个 _

void main() {
var (_, language, _) = (1, 'Dart', true);
print(language);
}

通配符常用于忽略记录(record)或模式匹配中不关心的部分。它表达的意图比创建一个普通变量后不使用更清晰。

版本说明:_ 的通配符语义适用于 Dart 3.7 及之后的语言版本。在更早的语言版本中,相关规则不同。

常见误区

误区一:认为 var 等于动态类型

var count = 1;

count 被推断为 int,之后不能赋值为字符串。需要动态行为时必须明确使用 dynamic,但通常应优先保留静态类型检查。

误区二:使用 late 掩盖合法的空状态

如果变量确实可能暂时没有值,应该声明为可空类型:

String? selectedItem;

只有能保证读取前一定初始化时,才使用:

late String selectedItem;

误区三:认为 final 对象完全不可变

final 只禁止变量重新赋值,对象是否可变由对象自身决定。final List 仍可添加元素,而 const List 不可修改。

误区四:所有不变的值都写成 const

DateTime.now()、文件内容、网络响应等值只能在运行时获得,即使后续不会改变,也只能使用 final,不能使用 const

如何选择声明方式

可以按照以下顺序判断:

  1. 值在编译时已确定且应保持不变:使用 const
  2. 值在运行时确定,但变量只赋值一次:使用 final
  3. 值稍后初始化且必须是非空:谨慎使用 late,只赋值一次时用 late final
  4. 值可以变化:使用显式类型或 var
  5. 值可以为 null:在类型后添加 ?
  6. 值的类型不确定:优先使用 Object? 并进行类型检查,确实需要动态调用时才使用 dynamic

小结

  • var 会推断出静态类型,并不等于 dynamic
  • 非空类型不能保存 null,可空类型使用 ?,例如 String?
  • Object? 保留静态检查,dynamic 会把很多错误推迟到运行时。
  • late 将初始化责任交给开发者,读取未初始化的 late 变量会在运行时报错。
  • final 表示只能赋值一次,值可以在运行时产生。
  • const 表示编译时常量,并可创建不可修改的常量对象。
  • TypeScript 的 const 更接近 Dart 的 final,不是 Dart const 的完全对应物。