Dart 3新特性—Records、模式匹配与switch表达式完全指南|新宇宙博客
返回列表Dart 3 新特性:模式匹配与记录 详解Dart 3 Records匿名元组、解构赋值应用、Pattern Matching的switch表达式、if-case、guard clause及exhaustiveness
Dart 3 新特性:模式匹配与记录
核心摘要 :Dart 3(2023年5月发布)引入了语言层面的模式匹配(Patterns) 、记录类型(Records) 、密封类(Sealed Classes) 和完善的穷举性 switch 。这些特性将 Dart 推向了函数式编程的新阶段,让代码更表达性强、更安全。本文深入剖析每个特性的语义、在 Flutter 中的实际应用,并与 Kotlin、Rust、Haskell 进行横向对比。
目录
记录类型(Records)
模式匹配基础
穷举性 switch 与密封类
解构(Destructuring)
模式的高级用法
与 Kotlin / Rust / TypeScript 的对比
实战案例:类型安全的状态机
深度追问
总结表格
1. 记录类型(Records)
Records 是不可变的匿名复合值类型 ,可以聚合多个值而无需定义专门的类。
1.1 基本语法
// 位置字段(Positional fields)
(int, String) point = (42, 'hello');
print(point.$1); // 42
print(point.$2); // hello
// 命名字段(Named fields)
({int x, int y}) vector = (x: 10, y: 20);
print(vector.x); // 10
print(vector.y); // 20
// 混合(位置 + 命名)
(String, {int age, String city}) user = ('Alice', age: 30, city: 'Beijing');
print(user.$1); // Alice
print(user.age); // 30
print(user.city); // Beijing
// Records 的相等性:按结构/值比较
(1, 'a') == (1, 'a'); // true!
(x: 1, y: 2) == (x: 1, y: 2); // true
// 注意:命名字段顺序不影响相等性
(x: 1, y: 2) == (y: 2, x: 1); // true(相同结构)
// hashCode 也自动正确实现
Set<(int, String)> set = {(1, 'a'), (1, 'a'), (2, 'b')};
print(set.length); // 2((1,'a') 重复被合并)
1.2 Records 作为多返回值 // ✅ Dart 3 之前:用 List、Map 或专门的类(不够优雅)
List<dynamic> divideOld(int a, int b) => [a ~/ b, a % b];
// ✅ Dart 3:用 Records,类型安全且清晰
(int quotient, int remainder) divide(int a, int b) {
return (a ~/ b, a % b);
}
void main() {
final (q, r) = divide(17, 5); // 解构赋值
print('商: $q, 余数: $r'); // 商: 3, 余数: 2
}
// 函数返回命名 Record
({String firstName, String lastName, int age}) parseFullName(String input) {
final parts = input.split(' ');
return (
firstName: parts[0],
lastName: parts.length > 1 ? parts[1] : '',
age: 0, // 默认值
);
}
// 异步也可以
Future<(bool success, String message)> login(String user, String pwd) async {
try {
await authService.login(user, pwd);
return (true, '登录成功');
} catch (e) {
return (false, e.toString());
}
}
void handleLogin() async {
final (success, message) = await login('alice', 'password');
if (success) {
print('✅ $message');
} else {
print('❌ $message');
}
}
1.3 Records vs 类 // Records:轻量、匿名、值语义(无法扩展/实现接口)
(double lat, double lng) location = (39.9042, 116.4074);
// Class:有名字、可扩展、引用语义、可添加方法
class Location {
final double lat;
final double lng;
const Location(this.lat, this.lng);
double distanceTo(Location other) { /* ... */ return 0; }
}
// 选择原则:
// Records:临时聚合、函数多返回值、局部数据传递
// Class:需要方法、需要继承/实现、作为 API 暴露的类型
2. 模式匹配基础
2.1 switch 表达式(新语法) // Dart 3 的 switch 表达式(有返回值)
String describe(Object value) => switch (value) {
int n when n < 0 => '负数',
int n when n == 0 => '零',
int n => '正整数: $n',
String s when s.isEmpty => '空字符串',
String s => '字符串: "$s"',
null => 'null',
_ => '其他类型: ${value.runtimeType}',
};
// switch 语句(执行副作用)
void handleEvent(Event event) {
switch (event) {
case ClickEvent(:final x, :final y):
print('点击 ($x, $y)');
case KeyEvent(:final key) when key == 'Enter':
print('按下 Enter');
case KeyEvent(:final key):
print('按下 $key');
case ScrollEvent(:final delta):
print('滚动 $delta');
}
}
2.2 模式类型总览 void patternExamples() {
// 1. 字面量模式(Literal Pattern)
const value = 42;
switch (value) {
case 0: print('零');
case 1 || 2 || 3: print('一到三'); // 逻辑或模式
case 42: print('答案');
}
// 2. 变量模式(Variable Pattern)
if (someValue case int n) {
print('是整数: $n'); // n 在此 scope 有效
}
// 3. 类型模式(Type Pattern)
switch (someObject) {
case String s: print('字符串长度: ${s.length}');
case int n: print('整数值: $n');
case List<int> list: print('整数列表: $list');
}
// 4. 通配符(Wildcard Pattern)
switch ((1, 2)) {
case (1, _): print('第一个是1,第二个随意');
}
// 5. 空检查模式(Null-check Pattern)
String? nullableStr = 'hello';
if (nullableStr case String s?) { // s? 匹配非null并绑定
print('非空字符串: $s');
}
// 6. 常量模式(Const Pattern)
const point = (1, 2);
switch (point) {
case (0, 0): print('原点');
case (var x, 0): print('x轴: $x');
case (0, var y): print('y轴: $y');
case (var x, var y): print('普通点: ($x, $y)');
}
}
3. 穷举性 switch 与密封类
3.1 密封类(Sealed Classes) // sealed:限制子类必须在同一文件中声明
sealed class Shape {
const Shape();
double area();
}
class Circle extends Shape {
final double radius;
const Circle(this.radius);
@override
double area() => 3.14159 * radius * radius;
}
class Rectangle extends Shape {
final double width, height;
const Rectangle(this.width, this.height);
@override
double area() => width * height;
}
class Triangle extends Shape {
final double base, height;
const Triangle(this.base, this.height);
@override
double area() => 0.5 * base * height;
}
// ✅ 穷举性检查:编译器知道 Shape 只有这三种子类
String describeShape(Shape shape) => switch (shape) {
Circle(:final radius) => '圆形,半径 $radius',
Rectangle(:final width, :final height) => '矩形 ${width}x$height',
Triangle(:final base, :final height) => '三角形,底 $base 高 $height',
// 无需 default!编译器验证所有情况已覆盖
};
// ⚠️ 如果添加新子类 Square,所有 switch 都会报编译警告/错误
// 这就是"密封"的价值:强制你更新所有分支
3.2 switch 的穷举性验证 // 配合 sealed class 使用,switch 表达式必须穷举
sealed class NetworkState {}
class NetworkIdle extends NetworkState {}
class NetworkLoading extends NetworkState {}
class NetworkSuccess<T> extends NetworkState {
final T data;
NetworkSuccess(this.data);
}
class NetworkError extends NetworkState {
final String message;
NetworkError(this.message);
}
// switch 语句(可以不穷举,但编译器会警告)
Widget buildFromState(NetworkState state) => switch (state) {
NetworkIdle() => const Text('空闲'),
NetworkLoading() => const CircularProgressIndicator(),
NetworkSuccess(:final data) => Text(data.toString()),
NetworkError(:final message) => Text('错误: $message',
style: const TextStyle(color: Colors.red)),
};
// ✅ 完全穷举,无需 default
4. 解构(Destructuring)
4.1 解构赋值 // 解构 Record
(int x, int y) point = (3, 4);
final (px, py) = point; // px=3, py=4
// 解构时可以忽略某些字段
final (first, _, third) = (1, 2, 3); // 忽略第二个
// 命名字段解构
({String name, int age}) person = (name: 'Alice', age: 30);
final (:name, :age) = person; // 变量名自动匹配字段名
// 解构 List
final [head, ...tail] = [1, 2, 3, 4, 5];
print(head); // 1
print(tail); // [2, 3, 4, 5]
final [first2, second, ...rest] = [10, 20, 30, 40];
// first2=10, second=20, rest=[30, 40]
// 解构 Map
final {'name': String name2, 'age': int age2} = {'name': 'Bob', 'age': 25};
print(name2); // Bob
// 解构 Object(类字段解构)
class Point {
final double x, y;
const Point(this.x, this.y);
}
void destructureObject(Point p) {
final Point(:x, :y) = p;
print('x=$x, y=$y');
}
// 在 for 循环中解构
final List<(String, int)> pairs = [('Alice', 90), ('Bob', 85), ('Carol', 92)];
for (final (name, score) in pairs) {
print('$name: $score');
}
4.2 模式在 if/while 中 // if-case 语句
void processValue(Object? value) {
if (value case int n when n > 0) {
print('正整数: $n');
} else if (value case String s when s.isNotEmpty) {
print('非空字符串: $s');
} else if (value case null) {
print('空值');
}
}
// while-case(较少见)
void processStream(Iterator<Object> iter) {
while (iter.moveNext() && iter.current case int n) {
print('处理整数: $n');
}
}
5. 模式的高级用法
5.1 守卫模式(Guard Clauses with when) sealed class Expr {}
class Num extends Expr { final double value; Num(this.value); }
class Add extends Expr { final Expr left, right; Add(this.left, this.right); }
class Mul extends Expr { final Expr left, right; Mul(this.left, this.right); }
// 递归模式匹配:表达式求值
double eval(Expr expr) => switch (expr) {
Num(:final value) => value,
Add(:final left, :final right) => eval(left) + eval(right),
// 乘法优化:任何数乘以0等于0(守卫优化)
Mul(left: Num(value: 0)) || Mul(right: Num(value: 0)) => 0,
Mul(:final left, :final right) => eval(left) * eval(right),
};
void testEval() {
// (2 + 3) * 4
final expr = Mul(Add(Num(2), Num(3)), Num(4));
print(eval(expr)); // 20.0
// 0 * (复杂表达式) = 0(优化路径)
final optimized = Mul(Num(0), Add(Num(100), Num(200)));
print(eval(optimized)); // 0.0(短路,不计算右边)
}
5.2 嵌套模式 // 嵌套解构 JSON
void parseConfig(Map<String, dynamic> json) {
switch (json) {
case {
'database': {
'host': String host,
'port': int port,
},
'cache': {'enabled': bool cacheEnabled},
}:
print('DB: $host:$port, Cache: $cacheEnabled');
case {'database': Map db}:
print('只有数据库配置: $db');
default:
print('无效配置');
}
}
// 模式的逻辑运算
void logicPatterns(Object value) {
switch (value) {
// 逻辑或(|)
case int n when n < 0 || n > 100:
print('超出范围: $n');
// 逻辑与(通过 when)
case int n when n >= 0 && n <= 100:
print('有效范围: $n');
}
}
6. 与 Kotlin / Rust / TypeScript 的对比
Kotlin 的 when
fun describe (value: Any ?) : String = when (value) {
is Int -> "整数: $value "
is String -> "字符串: $value "
null -> "null"
else -> "其他"
}
data class Point (val x: Int , val y: Int )
val (x, y) = Point(3 , 4 )
Rust 的模式匹配
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64 , height: f64 },
}
fn area (shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64 ::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}
if let Some (value) = optional_value {
println! ("有值: {}" , value);
}
TypeScript 的鉴别联合
type Shape =
| { kind : 'circle' ; radius : number }
| { kind : 'rect' ; width : number ; height : number };
function area (shape : Shape ): number {
switch (shape.kind ) {
case 'circle' : return Math .PI * shape.radius ** 2 ;
case 'rect' : return shape.width * shape.height ;
}
}
type Point = [number , number ];
const [x, y]: Point = [3 , 4 ];
7. 实战案例:类型安全的状态机 import 'dart:async';
// 使用 sealed class + 模式匹配实现类型安全的状态机
// 场景:用户认证流程
// 状态定义
sealed class AuthState {
const AuthState();
}
class AuthInitial extends AuthState {
const AuthInitial();
}
class AuthLoading extends AuthState {
const AuthLoading();
}
class AuthAuthenticated extends AuthState {
final String userId;
final String username;
final String accessToken;
const AuthAuthenticated({
required this.userId,
required this.username,
required this.accessToken,
});
}
class AuthUnauthenticated extends AuthState {
const AuthUnauthenticated();
}
class AuthError extends AuthState {
final String code;
final String message;
const AuthError({required this.code, required this.message});
}
// 事件定义
sealed class AuthEvent {
const AuthEvent();
}
class LoginRequested extends AuthEvent {
final String email;
final String password;
const LoginRequested(this.email, this.password);
}
class LogoutRequested extends AuthEvent {
const LogoutRequested();
}
class TokenRefreshRequested extends AuthEvent {
const TokenRefreshRequested();
}
// 状态机实现
class AuthBloc {
final _stateController = StreamController<AuthState>.broadcast();
AuthState _state = const AuthInitial();
Stream<AuthState> get stateStream => _stateController.stream;
AuthState get state => _state;
void add(AuthEvent event) {
_handleEvent(event);
}
Future<void> _handleEvent(AuthEvent event) async {
switch (event) {
case LoginRequested(:final email, :final password):
await _handleLogin(email, password);
case LogoutRequested():
_emit(const AuthUnauthenticated());
case TokenRefreshRequested():
await _handleTokenRefresh();
}
}
Future<void> _handleLogin(String email, String password) async {
_emit(const AuthLoading());
try {
final response = await authApi.login(email: email, password: password);
_emit(AuthAuthenticated(
userId: response.userId,
username: response.username,
accessToken: response.token,
));
} on AuthException catch (e) {
_emit(AuthError(code: e.code, message: e.message));
} catch (e) {
_emit(AuthError(code: 'UNKNOWN', message: e.toString()));
}
}
Future<void> _handleTokenRefresh() async {
// 只有在已认证状态下才能刷新
switch (_state) {
case AuthAuthenticated(:final accessToken):
try {
final newToken = await authApi.refreshToken(accessToken);
_emit(AuthAuthenticated(
userId: (_state as AuthAuthenticated).userId,
username: (_state as AuthAuthenticated).username,
accessToken: newToken,
));
} catch (e) {
_emit(const AuthUnauthenticated()); // Token 过期,需重新登录
}
case _:
// 非认证状态,忽略刷新请求
break;
}
}
void _emit(AuthState state) {
_state = state;
_stateController.add(state);
}
void dispose() => _stateController.close();
}
// Flutter UI 层:使用模式匹配构建 UI
class AuthWidget extends StatelessWidget {
final AuthState state;
const AuthWidget({super.key, required this.state});
@override
Widget build(BuildContext context) => switch (state) {
AuthInitial() => const SplashScreen(),
AuthLoading() => const LoadingScreen(),
AuthAuthenticated(:final username) => HomeScreen(username: username),
AuthUnauthenticated() => const LoginScreen(),
AuthError(:final message) => ErrorScreen(message: message),
};
}
// Records 在实战中的应用:HTTP 响应解析
Future<(int statusCode, T? data, String? error)> safeRequest<T>(
Future<T> Function() request,
) async {
try {
final data = await request();
return (200, data, null);
} on NotFoundException {
return (404, null, '资源不存在');
} on UnauthorizedException {
return (401, null, '未授权');
} catch (e) {
return (500, null, '服务器错误: $e');
}
}
void handleRequest() async {
final (statusCode, data, error) = await safeRequest(() => fetchUser(42));
switch (statusCode) {
case 200 when data != null:
print('成功: $data');
case 401:
redirectToLogin();
case 404:
showNotFound();
case _:
print('错误: $error');
}
}
8. 深度追问 Q1:Records 和 Map 有什么本质区别?
Records 是编译时类型安全 的:字段类型在编译时已知,访问时不需要类型转换。Map 是运行时动态的,map['key'] 返回 dynamic,容易出现类型错误。此外,Records 是值类型 (按结构相等),而 Map 是引用类型 (两个内容相同的 Map 不相等)。性能上 Records 也更好(栈分配,无 hash 计算)。
Q2:密封类(sealed)和枚举(enum)有什么区别?何时选哪个?
枚举(enum)适合有限的、无数据的 值集合(如方向、颜色、状态标志)。密封类(sealed)适合有限的、各自携带不同数据 的情况(如 UI 状态、网络结果、表达式节点)。Dart 3 的 enum 也支持携带字段和方法,进一步模糊了边界——若所有变体的数据结构相同,用 enum;若不同,用 sealed class。
Q3:switch 表达式和 switch 语句有什么区别?何时用哪个?
switch 表达式(switch (...) { case A => value, ... }):必须穷举 ,有返回值,适合在表达式位置使用(赋值、return、传参)。switch 语句(switch (...) { case A: ... }):不必穷举 ,无返回值,适合执行副作用。推荐优先用 switch 表达式,配合 sealed class 强制穷举,避免遗漏分支。
9. 总结表格 特性 Dart 3 Kotlin Rust TypeScript 记录类型 (T1, T2) ({K: T})data class(重量级)tuple (T1, T2) tuple [T1, T2] 模式匹配 switch + casewhenmatchswitch + 类型守卫穷举检查 ✅ sealed + switch 表达式 ✅ when + sealed ✅ 默认 ✅ noImplicitReturns 解构赋值 ✅ 内置 ✅ 解构声明 ✅ let 绑定 ✅ 解构赋值 守卫条件 whenwhen 条件if 守卫类型谓词 嵌套模式 ✅ 有限支持 ✅ 完整 有限