Variables(变量)
变量用于给数据起名字,并在程序中保存或引用数据。Dart 是静态类型语言:每个变量都有确定的类型,编译器会在运行前检查大部分类型错误。
学习 Dart 变量时,重点需要掌握以下问题:
- 如何使用显式类型或
var声明变量? - 变量能不能保存
null? Object?和dynamic有什么区别?late什么时候使用?final和const分别限制了什么?
声明变量
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 不通过let和var区分作用域规则。
let version = 3.12 // 推断为 number
version = '3.12' // 类型检查错误
变量的类型不会随赋值改变
变量的静态类型在声明后就已经确定。即使后续赋入的是某个子类型的对象,变量本身的静态类型也不会改变。
void main() {
num score = 90;
score = 95.5;
print(score);
}
num 是 int 和 double 的共同父类型,因此 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
}
这里的 String 和 String? 是不同的类型:
| 类型 | 可以保存的值 |
|---|---|
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'
Object、Object? 与 dynamic
当变量可能保存多种不同类型的值时,可以使用 Object、Object? 或 dynamic,但它们提供的类型安全程度不同。
Object 与 Object?
所有非空 Dart 对象都可以赋值给 Object。如果还需要允许 null,则使用 Object?。
void main() {
Object value = 'Dart';
value = 3;
Object? nullableValue = null;
nullableValue = true;
print(value);
print(nullableValue);
}
编译器只允许通过 Object 或 Object? 变量访问该静态类型已知的成员。使用具体类型的成员前,需要先进行类型检查:
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 类字段后的确定赋值断言
!可以告诉编译器该字段稍后会被初始化,但它本身不提供 Dartlate的运行时初始化检查,也不等同于惰性初始化。
只赋值一次: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
}
final 与 const 的选择
| 对比项 | final | const |
|---|---|---|
| 赋值次数 | 只能一次 | 只能一次 |
| 值的确定时间 | 可以在运行时 | 必须在编译时 |
| 引用的集合是否可修改 | 可以 | 不可以 |
| 常见用途 | 请求结果、当前时间、运行时配置 | 固定数字、固定文本、常量集合 |
选择时可以记住一句话:运行时确定但不再改变,用 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。
如何选择声明方式
可以按照以下顺序判断:
- 值在编译时已确定且应保持不变:使用
const。 - 值在运行时确定,但变量只赋值一次:使用
final。 - 值稍后初始化且必须是非空:谨慎使用
late,只赋值一次时用late final。 - 值可以变化:使用显式类型或
var。 - 值可以为
null:在类型后添加?。 - 值的类型不确定:优先使用
Object?并进行类型检查,确实需要动态调用时才使用dynamic。
小结
var会推断出静态类型,并不等于dynamic。- 非空类型不能保存
null,可空类型使用?,例如String?。 Object?保留静态检查,dynamic会把很多错误推迟到运行时。late将初始化责任交给开发者,读取未初始化的late变量会在运行时报错。final表示只能赋值一次,值可以在运行时产生。const表示编译时常量,并可创建不可修改的常量对象。- TypeScript 的
const更接近 Dart 的final,不是 Dartconst的完全对应物。