name: enumerable-refactor description: 这个技能应在用户请求重构 Ruby 代码以使用 Enumerable 方法替代手动迭代和空变量初始化时使用。当审查初始化空数组、哈希、字符串或计数器,然后通过 .each 循环填充的 Ruby 代码时使用。针对 Ruby 3.3+。当用户提及 “enumerable”、“refactor each”、“ruby iteration”,或在审查 Ruby 代码以进行惯用的集合处理时使用。
可枚举重构
概述
这个技能识别并重构最常见的 Ruby 反模式:初始化一个空变量并用 .each 填充它。几乎每次你看到这个模式,都有一个专门构建的 Enumerable 方法,它更简洁、表达性更强,并且通常性能更好。
核心反模式
# 任何时候你看到这种形状,它几乎肯定可以重构:
result = [] # 或 {}, 0, "", Set.new, false, nil
something.each do |item|
# ... 构建 result ...
end
result
何时使用此技能
应用当:
- 审查或重构使用
.each构建返回值的 Ruby 代码 - 用户要求使 Ruby 代码更符合习惯用法
- 用户提及 “enumerable”、“refactoring” 或 “each loop”
- 在代码审查中发现空变量然后each的模式
不要应用当:
.each循环纯粹用于副作用(发送邮件、写入数据库、打印)- 循环有复杂的控制流(
retry、异常处理)确实抵抗函数式表达 - 命令式版本对于特定情况确实更清晰
工作流程
1. 找到反模式
搜索代码库中的迹象:
# 搜索空数组初始化后跟 .each
Grep for: "= []\s*$" in *.rb 文件
Grep for: "= {}\s*$" in *.rb 文件
Grep for: "= 0\s*$" in *.rb 文件
Grep for: "= \"\"\s*$" in *.rb 文件
Grep for: "= Set.new" in *.rb 文件
Grep for: "= false\s*$" in *.rb 文件
然后检查这些初始化是否在几行内后跟 .each。
加载参考以获取完整模式目录:
读取 references/patterns.md
2. 分类每个实例
对于每个找到的反模式,识别 .each 循环在做什么:
| 循环行为 | 替换为 |
|---|---|
| 将每个元素转换为新数组 | map |
| 转换 + 扁平化嵌套数组 | flat_map |
| 转换 + 移除 nil | filter_map |
| 保留匹配条件的元素 | select / filter |
| 移除匹配条件的元素 | reject |
| 从集合构建哈希 | to_h { |x| [k, v] } |
| 用条件/复杂逻辑构建哈希 | each_with_object({}) |
| 按键分组元素 | group_by |
| 分成两组(true/false) | partition |
| 求和数字 | sum |
| 计数匹配 | count |
| 构建频率哈希 | tally |
| 找到第一个匹配 | find / detect |
| 按属性找到最小/最大 | min_by / max_by |
| 设置布尔标志 | any? / all? / none? |
| 连接字符串 | join 或 map { ... }.join |
| 累积单个不可变值 | reduce / inject |
3. 重构
对于每个实例,应用替换。显示前后。验证行为被保留。
关键规则:
- 对于可变累加器(哈希、数组、集合),使用
each_with_object,而不是inject - 只对不可变累加(数字、冻结字符串)使用
inject/reduce - 优先使用
filter_map而不是.select.map或.map.compact(单次遍历) - 优先使用
tally而不是Hash.new(0)+.each用于频率计数 - 对于简单的键值映射,优先使用
to_h { |x| [k, v] }而不是each_with_object({}) - 优先使用
partition而不是两个独立的select/reject调用 - 优先使用
sort_by而不是带比较块的sort
4. 检查链式机会
在单个重构后,寻找可以简化的方法链:
# 两次遍历 -> 一次遍历
users.select(&:active?).map(&:email)
# 变为
users.filter_map { |u| u.email if u.active? }
# 不必要的中间数组
departments.map(&:employees).flatten
# 变为
departments.flat_map(&:employees)
# 两次独立迭代用于最小和最大
lowest = temps.min
highest = temps.max
# 变为
lowest, highest = temps.minmax
5. 验证
- 确保重构后的代码产生相同结果
- 如果有测试,运行测试
- 检查细微差异(例如,
filter_map也移除false值,不仅仅是nil)
重要注意事项
-
filter_map也移除false:如果转换可以合法返回false,请使用.map.compact代替。 -
inject哈希错误:切勿使用inject构建哈希 —Hash#[]=返回值,而不是哈希。使用each_with_object代替。 -
惰性评估:对于非常大的集合,你只需要前 N 个结果,建议在链前使用
.lazy。 -
Rails 扩展:在 Rails 代码库中,也建议在适当情况下使用
index_by、index_with、pluck、excluding和sole。 -
副作用与
.each可以:不是每个.each都是反模式。如果循环体执行副作用并且不构建返回值,.each是正确的选择。