mcpField
D
dart-use-pattern-matching

dart-use-pattern-matching

github.com/flutter开发3.1k14.8k 次安装2 天前更新
代码仓库

关于 dart-use-pattern-matching

此技能为 Dart 开发者提供模式匹配与 switch 的选择和使用指导,适用于 JSON、路径、记录及密封类等数据处理。内容也强调穷尽性检查、快速失败和避免过度使用模式。

主要功能

  1. 01按数据结构选择模式
  2. 02用模式解构 JSON 与路径
  3. 03以 switch 表达式处理多态数据
  4. 04检查穷尽性并避免反模式

适用场景

  1. 01验证并提取 JSON 字段
  2. 02解析 URL 或路径片段
  3. 03穷尽处理密封类响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/flutter/agent-plugins --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