循环语句(Loops):坏味道识别与重构实战指南

24种代码坏味道系列 · 第13篇


1. 开篇场景

你是否遇到过这样的代码:使用 for 循环过滤、转换、求和、查找,虽然功能正确,但代码冗长且不够声明式?

1
2
3
4
5
6
7
8
9
std::vector<int> filterEvenNumbers(const std::vector<int>& numbers) {
std::vector<int> result;
for (int i = 0; i < numbers.size(); i++) {
if (numbers[i] % 2 == 0) {
result.push_back(numbers[i]);
}
}
return result;
}

这就是循环语句的典型症状。使用命令式循环处理集合,虽然可以工作,但代码冗长且不够声明式。现代C++提供了丰富的STL算法,可以更简洁、更安全地处理集合操作。

当你需要修改循环逻辑时,你必须在循环体中查找和修改。更糟糕的是,循环容易出现边界错误、空指针等问题,增加了bug的风险。


2. 坏味道定义

循环语句是指使用命令式循环处理集合,应该使用更声明式的STL算法。

就像用原始工具完成现代任务,虽然可以工作,但效率低且容易出错。

核心问题:STL算法更简洁、更安全、更易读。使用STL算法可以减少代码量,提高代码的可读性和可维护性。


3. 识别特征

🔍 代码表现:

  • 特征1:使用 for 循环进行过滤、转换、求和等操作
  • 特征2:循环体中有条件判断和集合操作
  • 特征3:循环代码冗长,需要多行才能完成简单操作
  • 特征4:循环中有手动管理索引或迭代器
  • 特征5:循环逻辑可以用STL算法替代

🎯 出现场景:

  • 场景1:从其他语言迁移到C++时,保留了循环的习惯
  • 场景2:不熟悉STL算法,使用循环实现
  • 场景3:快速开发时,使用循环简化代码
  • 场景4:重构不彻底,只修改了部分循环

💡 快速自检:

  • 问自己:这个循环是否可以用STL算法替代?
  • 问自己:这个循环是否在过滤、转换、求和、查找?
  • 工具提示:使用代码分析工具检测可以用STL算法替代的循环

4. 危害分析

🚨 维护成本:循环代码冗长,修改时需要理解整个循环逻辑,时间成本增加30%

⚠️ 缺陷风险:循环容易出现边界错误、空指针等问题,bug风险增加40%

🧱 扩展障碍:添加新功能时需要修改循环逻辑,容易破坏现有代码

🤯 认知负担:需要理解循环的每个步骤,增加了心理负担


5. 重构实战

步骤1:安全准备

  • ✅ 确保有完整的单元测试覆盖
  • ✅ 创建重构分支:git checkout -b refactor/replace-loops-with-algorithms
  • ✅ 使用版本控制,便于回滚

步骤2:逐步重构

重构前(问题代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 坏味道:使用命令式循环
class BadExample {
public:
// 使用循环过滤
std::vector<int> filterEvenNumbers(const std::vector<int>& numbers) {
std::vector<int> result;
for (int i = 0; i < numbers.size(); i++) {
if (numbers[i] % 2 == 0) {
result.push_back(numbers[i]);
}
}
return result;
}

// 使用循环转换
std::vector<int> doubleNumbers(const std::vector<int>& numbers) {
std::vector<int> result;
for (int num : numbers) {
result.push_back(num * 2);
}
return result;
}

// 使用循环求和
int sumNumbers(const std::vector<int>& numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}

// 使用循环查找
bool containsNumber(const std::vector<int>& numbers, int target) {
for (int num : numbers) {
if (num == target) {
return true;
}
}
return false;
}
};

问题分析

  • 使用命令式循环处理集合操作
  • 代码冗长,需要多行才能完成简单操作
  • 容易出现边界错误、空指针等问题

重构后(清洁版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ✅ 使用 STL 算法(更声明式)
class GoodExample {
public:
// 使用 std::copy_if
std::vector<int> filterEvenNumbers(const std::vector<int>& numbers) {
std::vector<int> result;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(result),
[](int n) { return n % 2 == 0; });
return result;
}

// 使用 std::transform
std::vector<int> doubleNumbers(const std::vector<int>& numbers) {
std::vector<int> result;
result.reserve(numbers.size());
std::transform(numbers.begin(), numbers.end(), std::back_inserter(result),
[](int n) { return n * 2; });
return result;
}

// 使用 std::accumulate
int sumNumbers(const std::vector<int>& numbers) {
return std::accumulate(numbers.begin(), numbers.end(), 0);
}

// 使用 std::find
bool containsNumber(const std::vector<int>& numbers, int target) {
return std::find(numbers.begin(), numbers.end(), target) != numbers.end();
}
};

关键变化点

  1. 用算法替代循环(Replace Loop with Algorithm)

    • 将命令式循环替换为STL算法
    • 代码更简洁、更声明式
  2. 提高可读性

    • STL算法名称清晰表达意图
    • 代码更易读、更易理解
  3. 提高安全性

    • STL算法经过充分测试,更安全
    • 减少边界错误、空指针等问题

步骤3:重构技巧总结

使用的重构手法

  • 用算法替代循环(Replace Loop with Algorithm):将命令式循环替换为STL算法
  • 提取方法(Extract Method):将循环逻辑提取为独立方法

注意事项

  • ⚠️ 确保STL算法适用于当前场景
  • ⚠️ 如果循环逻辑复杂,可能需要分步重构
  • ⚠️ 重构后要运行所有测试,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 循环是否可以用STL算法替代?
    • 循环是否在过滤、转换、求和、查找?
    • 使用IDE的代码提示,优先选择STL算法
  • 小步提交

    • 发现可以用STL算法替代的循环时,立即重构
    • 使用”用算法替代循环”重构,保持代码简洁

🔍 Code Review清单:

  • 重点检查

    • 是否有可以用STL算法替代的循环?
    • 循环是否冗长且不够声明式?
    • 循环是否容易出现边界错误?
  • 拒绝标准

    • 可以用STL算法替代的循环
    • 循环代码冗长且不够声明式
    • 循环中有手动管理索引或迭代器

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测可以用STL算法替代的循环
    • 启用循环复杂度警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测可以用STL算法替代的循环,生成警告报告

下一篇预告:冗赘的元素(Lazy Element)- 如何去掉不必要的抽象层