Dart 3 引入了几个极其强大的语言特性,其中最耀眼、对我们日常开发改变最大的就是 记录(Records) 和 密封类(Sealed classes),它们配合 模式匹配(Pattern Matching),让 Dart 的表现力达到了现代编程语言(如 Swift、Kotlin、Rust)的水平。
下面为你详细介绍这两个特性以及它们在 Flutter 开发中的实战场景:
一、 记录(Records)
记录 是一种匿名的、不可变的、轻量级的数据结构,允许你将多个不同类型的值捆绑在一起。
在 Dart 3 之前,如果一个函数需要返回多个值,我们要么定义一个专门的类,要么使用第三方库(如 tuple 或 dartz),非常麻烦。有了 Records,一切变得极其简单。
1. 基本语法
你可以使用小括号 () 来创建记录,它可以包含位置字段和命名字段:
// 1. 位置字段 (Positional fields)
var userInfo = ('Alice', 25);
print(userInfo.$1); // 输出: Alice
print(userInfo.$2); // 输出: 25
// 2. 命名字段 (Named fields)
var product = (name: 'MacBook', price: 1999.0);
print(product.name); // 输出: MacBook
print(product.price); // 输出: 1999.0
// 3. 混合使用
var combined = ('Success', code: 200);
print(combined.$1); // 输出: Success
print(combined.code); // 输出: 200
2. 最强用法:解构(Destructuring)
Records 真正的威力在于配合模式匹配进行解构赋值。当你从函数获取多返回值时,可以直接在声明时拆解它们:
// 定义一个返回 Record 的函数
(double lat, double lon) getLocation() {
return (34.0522, -118.2437);
}
void main() {
// 直接解构返回值,不需要通过 $1, $2 访问
var (latitude, longitude) = getLocation();
print('纬度: $latitude, 经度: $longitude');
}
实战意义:彻底淘汰了 Tuple 库。在处理 API 请求时,经常需要同时返回“数据”和“分页信息”,现在直接用 (List<Data>, PageInfo) 即可。
二、 密封类(Sealed Classes)
密封类 是一种特殊的类,它限制了类的继承体系。当你声明一个类为 sealed 时,它的所有子类都必须定义在同一个文件中。
这听起来只是个限制,但它的目的是为了让编译器知道这个类到底有多少个子类(穷举性 Exhaustiveness)。
1. 核心优势:穷举模式匹配 (Exhaustive Pattern Matching)
在 Dart 3 之前,处理不同状态(如加载中、成功、失败)通常用 enum,但 enum 不能携带复杂数据。如果用普通抽象类,在 switch 时编译器无法保证你处理了所有情况,往往被强制要求写一个没用的 default 分支。
有了 sealed,配合 Dart 3 的 switch 表达式,不仅可以携带数据,还能得到编译器的绝对安全保证。
// 1. 定义一个密封类及其在同一个文件下的子类
sealed class AuthState {}
class Unauthenticated extends AuthState {}
class Loading extends AuthState {}
class Authenticated extends AuthState {
final String userName;
final String token;
Authenticated(this.userName, this.token); // 可以携带复杂数据!
}
class Error extends AuthState {
final String message;
Error(this.message);
}
// 2. 使用 switch 表达式处理状态
Widget buildAuthUI(AuthState state) {
// 注意:这里是一个 switch 表达式,直接返回值
return switch (state) {
Unauthenticated() => const LoginScreen(),
Loading() => const CircularProgressIndicator(),
// 自动解构出 Authenticated 类里的参数
Authenticated(userName: var name) => WelcomeScreen(name: name),
Error(:var message) => ErrorScreen(message: message), // 简写语法
};
}
编译器的魔法:在上面的 switch 中,如果你漏写了 Loading() 的情况,代码会直接编译报错,提示你 "Non-exhaustive switch" (未穷尽的 switch)。这意味着你永远不会因为忘记处理某个状态而在运行时遇到 Bug。
2. 实战意义:替代部分 Freezed 和 Dartz 功能
如果你用过 freezed 的 @freezed 联合类型(Union types)和 .when() / .map() 方法,或者 dartz 的 Either<Left, Right>,你会发现 Dart 3 的 sealed class 配合 switch 完美地在语言原生层面实现了相同的功能。
例如,实现原生的 Result 类型(替代 Either):
sealed class Result<T, E> {}
class Success<T, E> extends Result<T, E> {
final T data;
Success(this.data);
}
class Failure<T, E> extends Result<T, E> {
final E error;
Failure(this.error);
}
// 使用
String handleApi(Result<User, Exception> result) {
return switch (result) {
Success(:var data) => '欢迎, ${data.name}',
Failure(:var error) => '出错了: $error',
};
}
总结
- Records(记录):轻量级地组合和返回多个数据,无需定义冗余的数据类。
- Sealed Classes(密封类):创建安全的、可控的类层级,完美表达“状态机”。
- 两者结合(模式匹配):让 Flutter 开发者能够用原生语法写出极度安全、精简的 UI 状态分发逻辑和业务逻辑,大幅度降低了对外部库(如
freezed中的联合类型特性、dartz、tuple)的依赖。这就是为什么目前推荐的 Flutter 项目都会强调 "要求 Dart 3+" 的原因。
关键维度横向对比表
| 特性 | Record | List | Class 对象 |
|---|---|---|---|
| 长度 | ✅ 固定,编译确定 | ❌ 动态可变 | 字段数量由类定义固定 |
| 访问方式 | $1/$2(位置)、自定义命名 | 数字下标 [index] |
. 成员名 .name |
| 值相等 == | ✅ 原生支持深度值比较 | ❌ 默认引用比较 | ❌ 默认引用比较,需手动重写 |
| 是否可添加 / 删除元素 | ❌ 不能增删字段 | ✅ 可变 List 支持 add/remove | ❌ 不能动态新增字段 |
| 能否拥有方法 | ❌ 只能存数据 | ✅ 大量内置方法 | ✅ 自定义任意方法 |
| 类型安全 | 极强,每个位置独立类型 | 统一泛型类型 List<T> |
强类型,自定义成员类型 |
| 可变能力 | Record 本身不可变;内部对象可变 | 可变 / 不可变两种模式 | 看字段是否 final |
| 解构支持 | ✅ 原生解构 | ✅ 支持解构 | ❌ 不支持原生解构(Dart 暂不支持对象解构) |
| 适用场景 | 临时多返回值、简易数据打包 | 一组同类型集合(列表、数组) | 业务实体、复杂数据模型 |