关于 dart-use-pattern-matching
dart-use-pattern-matching 是面向 Dart 开发者的编码技能,指导如何对数据、路径和密封类型使用模式匹配与 switch。内容结合选择规则、反模式、验证建议和示例。
主要功能
- 01按数据结构选择模式
- 02switch 表达式与语句指南
- 03Map、List、Record 和 Object 模式
- 04完备性与运行时验证检查
- 05实用模式匹配的反模式
适用场景
- 01反序列化并验证 JSON 数据
- 02解析 URL 或路径片段
- 03完备处理密封响应类型
- 04解构多个返回值
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dart-lang/ skills --skill dart-use-pattern-matching安装后在 Claude Code 中输入/dart-use-pattern-matching确认已加载。
附带文件examples/json_patterns.dart
详细介绍
实现 Dart 模式
目录
模式选择策略
根据数据结构和预期结果应用特定模式类型。遵循以下条件指南:
- 如果要验证并从反序列化数据(例如 JSON)中提取内容: 使用 Map、List 和 Object 模式,在单个步骤中验证架构结构并解构属性。
- 如果要检查 URL/路径片段(
uri.pathSegments、p.split(path))或String.split()生成的令牌: 使用带有剩余元素的 List 模式(['api', 'comments', ...]、['assets', ...final rest]、[..., final parent, _]),而不是手动检查.length、使用.first、.skip(1)或通过length - N计算索引。 - 如果要处理多态负载或响应: 使用针对 Map 判别键的
switch表达式,将数据反序列化为sealed类层次结构。 - 如果要处理多个返回值: 使用 Record 模式,将字段直接解构到局部变量中。
- 如果要从对象或
MapEntry中解包多个字段: 如果需要提升可空属性,或在多个语句中使用 3 个及以上字段,请使用 Object 模式(final Spec(:field1, :field2!) = spec;、for (final MapEntry(:key, :value) in map.entries)),而不是用于单次调用的直接传递。 - 如果要执行特定于类型的行为(代数数据类型): 将 Object 模式与
sealed类结合使用,以确保穷尽处理。 - 如果要匹配数值范围或条件: 在 switch 分支中使用关系模式(
>=、<=)和逻辑与模式(&&)。 - 如果多个分支共享逻辑: 使用逻辑或模式(
||)共享同一个分支主体或保护子句。 - 如果要忽略特定值: 使用通配符模式(
_)或集合中不匹配的剩余元素(...)。
Switch 语句与表达式
根据执行上下文选择合适的 switch 构造:
- 如果要生成一个值: 使用 switch 表达式。
- 语法:
switch (value) { pattern => expression, } - 规则:每个分支必须是单个表达式。不会隐式贯穿。必须穷尽所有情况。
- 语法:
- 如果要执行语句或产生副作用: 使用 switch 语句。
- 语法:
switch (value) { case pattern: statements; } - 规则:空分支会贯穿到下一个分支。非空分支会隐式终止(无需
break关键字)。
- 语法:
核心模式实现
按照以下语法和规则实现模式:
- 逻辑或(
||):pattern1 || pattern2。两个分支必须定义完全相同的变量集合。 - 逻辑与(
&&):pattern1 && pattern2。分支不得定义相互重叠的变量。 - 关系运算:
==、!=、<、>、<=、>=后跟一个常量表达式。 - 类型转换(
as):pattern as Type。如果值与类型不匹配,则抛出异常。用于在解构时强制断言类型。 - 空值检查(
?):pattern?。如果值为 null,则匹配失败。将变量绑定为非空基类型。 - 空值断言(
!):pattern!。如果值为 null,则抛出异常。 - 变量:
var name或Type name。将匹配的值绑定到新的局部变量。 - 通配符(
_): 匹配任何值并将其丢弃。 - 列表:
[pattern1, pattern2]。匹配长度完全一致的列表,除非使用 Rest 元素(...或...var rest)。 - 映射:
{"key": pattern}。匹配包含指定键的映射。忽略未匹配的键。 - 记录:
(pattern1, named: pattern2)。匹配形状完全一致的记录。使用:var name推断 getter 名称。 - 对象:
ClassName(field: pattern)。匹配ClassName的实例。使用:var field推断 getter 名称。
务实权衡与反模式
模式匹配和 switch 表达式应简化代码,而不是增加语法开销。请遵循以下界限:
1. 单个可提升变量优先使用 is 类型提升,而非 if-case
检查或提升单个变量时,请使用标准的 is 检查,而不是引入别名的 if-case 模式。
- 推荐:
// ✅ 直接在原处提升 `key`,无需额外变量 for (final MapEntry(:key, :value) in map.entries) { if (key is String && value != null) { process(key, value); } } - 避免:
// ❌ 反模式:引入了不必要的别名变量 `k` for (final MapEntry(:key, :value) in map.entries) { if (key case final String k when value != null) { process(k, value); } }
2. 在 switch 分支中合并可空类型
当映射或返回值时,如果 null 和类型 T 都是有效值且处理方式相同,应直接匹配可空类型 T?,而不是创建多余的 null 分支。
- 推荐:
// ✅ 简洁的可空模式匹配 switch (value) { final String? s => s, _ => throw FormatException('Invalid value: $value'), } - 避免:
// ❌ 多余的独立 null 分支 switch (value) { final String s => s, null => null, _ => throw FormatException('Invalid value: $value'), }
3. 保留快速失败验证(不要静默丢弃数据)
如果格式错误的数据应触发错误或诊断警告,请勿在循环或反序列化中使用 if-case 过滤元素。
- 推荐:
// ✅ 快速失败并明确报告诊断错误 for (final raw in rawTasks) { if (raw is! Map<String, dynamic>) { throw FormatException('Expected Map item, got ${raw.runtimeType}: $raw'); } _applyTask(raw); } - 避免:
// ❌ 静默忽略格式错误的项 for (final raw in rawTasks) { if (raw case final Map<String, dynamic> taskMap) { _applyTask(taskMap); } }
4. 避免单分支或布尔值 switch
- 只有一个分支且带有
default: break;的 switch 语句,应改用if (x is T)。 - 应使用标准条件三元运算符(
condition ? a : b),而不是switch (condition) { true => a, false => b }。
5. 使用对象解构解包多个字段;不要用于单个属性或直接传递
在以下情况下,使用对象或记录解构:提取 3 个或更多字段,并在多条语句中使用;在 map.entries 循环中迭代 MapEntry(:key, :value);消除只使用一次的中间变量;或将可空字段提升为局部变量(使用 :field!,或使用 :field 并配合 != null)。以下情况应保留标准属性/元素访问(config.field):
-
读取单个属性(
user.name,或使用final CpuCallFrame(:url, :line) = node.callFrame;,而不是嵌套ProfileNode(callFrame: ...))。 -
在简短的 getter/方法中只解包 1–2 个字段,或只解包字段并立即将它们传入单个构造函数或函数调用。
-
显式局部类型注解(
specify_nonobvious_local_variable_types)要求为边际(1–2 倍)字段复用添加新的库导入。 -
较长的类名搭配重命名的 getter 时,将 2 个字段的解包拆成 3 行或更多行。
-
任何目标变量都会被赋值给外层作用域的变量(例如,在
finally之前使用db = res.$1;——final (db, ...)会遮蔽外层的db),或混用final和可变的var局部变量。 -
读取长度可能为 1 的列表中的
.first和.last([first, ..., last]要求length >= 2;不要改用多分支if-case)。 -
优先使用:
// ✅ 在一次声明中解包多个字段并断言非空 final RunSpec(:target, :device!, :verbose) = spec; -
避免使用:
// ❌ 重复的多行属性提取 final target = spec.target; final device = spec.device!; final verbose = spec.verbose;
6. 避免将 if-case 用于独立的标量比较
独立条件应使用标准布尔运算符(if (code >= 200 && code < 300)),而不是 if (code case >= 200 && < 300)。关系模式应保留给多分支 switch 表。
工作流
任务进度:实现模式匹配
实现复杂的模式匹配逻辑时,复制此清单以跟踪进度:
- 确定正在评估的数据结构(JSON、List/Segments、Record、Class、Enum)。
- 选择合适的 switch 构造(对值使用 Expression,对副作用使用 Statement)。
- 定义所需的模式(Object、Map、List、Record)。
- 使用变量模式提取所需数据(
var x、:var y、...final rest)。 - 对无法用模式表达的逻辑应用 Guard 子句(
when condition)。 - 使用通配符(
_)或default子句处理不匹配的情况(未使用密封类时)。 - 运行静态分析器检查穷尽性和死代码(
dart analyze)。 - 验证运行时 Map/JSON 模式对省略键(
containsKey语义)与显式null值的处理行为。
反馈循环 1:穷尽性检查(静态验证)
对 sealed 类或枚举执行 switch 时,确保所有子类型都在编译时得到处理:
- 运行分析器: 执行
dart analyze。 - 检查错误: 查找“The type 'X' is not exhaustively matched by the switch cases”或不可达模式分支警告。
- 修复: 为未处理的子类型添加缺失的 Object 模式;如果可以接受默认回退或错误,则添加显式通配符(
_)分支。
反馈循环 2:运行时 Map 和 JSON 模式验证
由于 dart analyze 无法静态验证动态 Map<String, dynamic> 键,因此请明确验证运行时模式语义:
- 省略键与显式
null:Map 模式{'key': String? val}会检查map.containsKey('key')。如果 JSON 负载中省略了'key',即使String?可空,该模式在运行时也会匹配失败。应直接从已验证的 map 中提取可选键(map['key'] as String?)。 - 快速失败回退:确保不匹配或格式错误的 map 结构会进入显式的
_ => throw FormatException(...)分支,而不是在if-case检查中静默失败。
示例
多态 JSON 反序列化(判别联合)
使用 Map 模式搭配 switch 表达式,验证带标签的 JSON 载荷并构造 sealed 类层次结构。请参阅
examples/json_patterns.dart,其中提供了可执行实现,演示如何将带标签的 ApiResponse 解析为 SuccessResponse
和 ErrorResponse。
嵌套 JSON 验证与可选字段
使用嵌套的 Map 和 List 模式,在单一步骤中验证必需的架构结构并提取集合。请参阅
examples/json_patterns.dart,其中提供了 processUserPayload 的可执行实现。
Map 模式会检查键是否存在(containsKey)。如果可选 JSON 键可能完全未出现在载荷中(而不是显式传入
'key': null),请通过模式解构必需键,并直接从匹配的子 Map 中提取可选字段。
List 和路径片段解构
使用 if-case 和 List 剩余元素(...final rest)验证前缀并提取其余元素,无需使用包含 2 个分支的 _ => null switch,也无需手动通过 .first
和 .skip(1) 进行索引。
String? resolveAllowedAssetSubpath(List<String> segments) {
if (segments case ['assets', ...final rest]
when rest.isNotEmpty && !rest.contains('..')) {
return rest.join('/');
}
return null;
}
代数数据类型(封闭类)
使用 Object 模式搭配 switch 表达式,穷尽处理各类类型。
sealed class Shape {}
class Square implements Shape {
final double length;
Square(this.length);
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
}
// Switch expression guarantees exhaustiveness due to `sealed` modifier.
double calculateArea(Shape shape) => switch (shape) {
Square(length: var l) => l * l,
Circle(:var radius) => math.pi * radius * radius,
};
变量交换与解构
使用变量赋值模式交换值,或提取记录字段,无需临时变量。
var (a, b) = ('left', 'right');
(b, a) = (a, b); // Swap values
// Destructuring a function return
var (name, age) = getUserInfo();
守卫子句与逻辑或
使用 when 在模式匹配成功后计算任意条件。
switch (shape) {
case Square(length: var s) || Circle(radius: var s) when s > 0:
print('Valid positive shape with dimension $s');
case Square() || Circle():
print('Zero or negative dimension shape');
}