Dart变量与类型系统深度解析
从var与dynamic的辨析到Sound Null Safety的设计哲学,拆解Dart类型系统的核心机制。
一句话概括
Dart 的类型系统以 Sound Null Safety 为基石,通过编译时的类型推断和运行时的健全保障,做到「所有 null 错误在编译期就被消灭」——这是它区别于 TypeScript 和其他移动端语言的核心优势。
核心知识点
1. var vs dynamic——面试第一问
这是 Dart 新手最容易踩的坑,也是面试必问。一句话区分:
1
2
3
4
5
6
7
8
var a = 'hello'; // a 的类型在编译时被锁定为 String,一辈子不变
dynamic b = 'hello'; // b 的类型是「运行时再说」,可以随意切换
// a = 42; // ❌ 编译错误:String 不能赋 int
b = 42; // ✅ 运行时不报错
print(a.length); // ✅ IDE 有补全,编译器帮你检查
print(b.length); // ✅ 编译通过(但运行时 b 可能是 int → crash)
核心区别: var 是把安全性前移到编译时;dynamic 是把所有检查推迟到运行时——就像解开安全带开车,不出事时很爽,出事就是大问题。
什么时候用 dynamic? 只有和 JS 互操作、解析不知道格式的 JSON 时。其他所有场景都应该用 var、具体类型或 Object? 替代。
2. const vs final——编译时 vs 运行时
1
2
3
4
5
6
7
8
9
10
const pi = 3.14159; // ✅ 编译时常量,值在编译时就确定了
final now = DateTime.now(); // ✅ 运行时确定,但确定后不可变
// const now2 = DateTime.now(); // ❌ DateTime.now() 是运行时才能知道的
// const 在 Flutter 中的性能意义:
const Padding(padding: EdgeInsets.all(16)); // 同一个实例被复用 1000 次
// vs
Padding(padding: EdgeInsets.all(16)); // 1000 次创建 1000 个不同实例
// Widget 重建时,const widget 的 Element 直接跳过重建——性能提升显著
记忆口诀: const 是「代码还没跑的值」,final 是「跑第一次后不再变的值」。
3. Sound Null Safety——Dart 的杀手锏
Dart 的空安全是「健全的」(Sound),意思不是「差不多不会 null」,而是数学上可证明的绝对不会 null。这是 TypeScript 做不到的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
String name = 'Alice'; // name 永远不会是 null
String? maybeName = null; // 问号 = 允许 null
// 使用可空变量前必须解包:
void printLength(String? text) {
// text.length; // ❌ 编译错误:text 可能是 null
if (text != null) {
print(text.length); // ✅ 类型收窄:在 if 里 text 就是 String 了
}
print(text?.length ?? 0); // ✅ 空安全链式调用 + 默认值
// print(text!.length); // ⚠️ 断言非空,但如果错了会抛运行时异常
}
对比 TypeScript: TS 的 strictNullChecks 只在编译时提示,运行时照样能穿 null 进去(as any 绕过)。Dart 是真·类型安全保障。
4. Dart 的类型体系结构
1
2
3
4
5
6
7
Object? (顶级类型,包括 null)
/ \
Object Null
/ | \
num String bool ...
/ \
int double
关键区别: Object 和 Object? 是两个不同的类型——前者不能是 null,后者可以。这和 TS 的 object | null 不是一回事,Dart 的类型系统从根上把 null 和非 null 分开了。
5. late——延迟初始化但承诺非空
1
2
3
4
5
6
7
8
9
10
11
12
class Database {
late final String connectionString; // 「我保证在使用前初始化」
void init(String host) {
connectionString = 'https://$host'; // 第一次赋值
// connectionString = 'other'; // ❌ final 不能二次赋值
}
void connect() {
print(connectionString); // 如果 init 没调 → LateInitializationError
}
}
使用场景: 依赖注入、initState() 中初始化、从 JSON 反序列化。用 late 的好处是不需要每次访问都写 ! 断言。
其实你每天都在用
- Flutter 里的
const:const Text('hello')和Text('hello')看起来一样,但前者在 Widget 树重建时直接被框架跳过——这就是为什么有人说「多写 const,性能提升 20%」 - JSON 解析的
dynamic灾难:json['count']返回dynamic,直接.toInt()在值不是 int 时炸——这就是为什么json_serializable要生成类型安全的 fromJson final user = await fetchUser()永远不会变: 不像 JS 的const(只锁引用),Dart 的final带类型安全——编译器保证你不会把 User 当 String 用- 空安全迁移前项目 crash 最多的是
Null check operator used on a null value: 迁移后这个错误在编译时就被拦截了 late变量让你不用在initState里写late: 不需要User? _user;然后到处写_user!——late final User _user;干净得多
常见误解(FAQ)
❌ 误区1:「Dart 有类型推断,所以不需要写类型注解」
类型推断省的是赋值场景的注解(var x = 42 编译器知道是 int)。但函数参数、公开 API 的返回值必须显式写类型——这是代码可读性的底线。getData() 返回什么?只有显式写 Future<User> getData() 才算清楚。
❌ 误区2:「var 和 dynamic 差不多,dynamic 还更灵活」
var 是类型推断,dynamic 是放弃类型检查。它们的相似性仅在一个场景:都写 var x = ... 和 dynamic x = ...。但后续使用完全不同——var 有 IDE 补全和编译检查,dynamic 等同于裸奔。
❌ 误区3:「final 就是 Dart 里的 const」
const = 编译时常量 + 实例规范化(同一个值共享同一个对象);final = 运行时不可变。final list = [1,2,3]; list.add(4) 是合法的——final 锁的是引用,不锁内容。
❌ 误区4:「开空安全就够了,不需要额外处理」
空安全消灭了「忘记判空」的错误,但它不消灭业务层需要 null 表达「没有数据」的场景。String? 让你必须处理 null 但不要矫枉过正——?? 和 ?. 就是你处理 null 的工具,不是 bug。
一句话总结
Dart 的类型系统不是在限制你,而是在替你挡枪——var 给你推断、const 给你性能、Sound Null Safety 帮你消灭线上 crash,这些机制加起来就是「写的时候多想一步,上线后少救一场火」。