文章

Dart变量与类型系统深度解析

从var与dynamic的辨析到Sound Null Safety的设计哲学,拆解Dart类型系统的核心机制。

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,这些机制加起来就是「写的时候多想一步,上线后少救一场火」。

本文由作者按照 CC BY 4.0 进行授权