mcpField
D
dart-use-pattern-matching

dart-use-pattern-matching

github.com/dart-lang开发53219.0k 次安装2 周前更新
代码仓库

关于 dart-use-pattern-matching

dart-use-pattern-matching 是面向 Dart 开发者的编码技能,指导如何对数据、路径和密封类型使用模式匹配与 switch。内容结合选择规则、反模式、验证建议和示例。

主要功能

  1. 01按数据结构选择模式
  2. 02switch 表达式与语句指南
  3. 03Map、List、Record 和 Object 模式
  4. 04完备性与运行时验证检查
  5. 05实用模式匹配的反模式

适用场景

  1. 01反序列化并验证 JSON 数据
  2. 02解析 URL 或路径片段
  3. 03完备处理密封响应类型
  4. 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 时,确保所有子类型都在编译时得到处理:

  1. 运行分析器: 执行 dart analyze。
  2. 检查错误: 查找“The type 'X' is not exhaustively matched by the switch cases”或不可达模式分支警告。
  3. 修复: 为未处理的子类型添加缺失的 Object 模式;如果可以接受默认回退或错误,则添加显式通配符(_)分支。
反馈循环 2:运行时 Map 和 JSON 模式验证

由于 dart analyze 无法静态验证动态 Map<String, dynamic> 键,因此请明确验证运行时模式语义:

  1. 省略键与显式 null:Map 模式 {'key': String? val} 会检查 map.containsKey('key')。如果 JSON 负载中省略了 'key',即使 String? 可空,该模式在运行时也会匹配失败。应直接从已验证的 map 中提取可选键(map['key'] as String?)。
  2. 快速失败回退:确保不匹配或格式错误的 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');
}

评论

支持 Markdown