“C++代码冗余消除”这个题目,很多搞C++的都见过,也都头疼过。项目规模一上来,代码里总会出现各种看着别扭、删了又怕出事的“赘肉”。可真正棘手的地方在于:哪些冗余值得花时间清理,哪些清理反而会给自己挖坑,很难一言说清。这篇博文就聚焦C++项目的冗余消除实操,结合我在大型代码库中踩过的一些坑,从冗余类型识别、优先级评估、手工重构手法,到静态分析工具配置和收益度量,都给出能直接“抄作业”的参考方案。适合正在做代码重构、想降低维护成本,或者准备系统性治理技术债的团队与个人开发者。
1. 代码冗余的七种常见面孔
先说一个我见过很多次的场景:某大型C++服务端项目,编译一次十几分钟,二进制体积快两百兆。负责人天天说“想精简,但不敢动”,因为谁都不知道删了某段“没用”的代码,会不会在某个角落里炸出个运行时错误。这种担忧其实源自同一个问题——对冗余的形态缺乏系统分类。先把冗余看清楚,才能对症下药。
1.1 重复代码:最显眼的“肉山”
重复代码是最容易被识别的冗余。三个模块里,各粘贴了一份几乎一样的字符串拼接逻辑,只是参数名不同;两个类里各自实现了相似度九成以上的链表遍历接口;面试时写过的那个“手写String类”,在生产代码里居然也有一份带着小改动、无人在用的版本。这些直接或近似的复刻,是冗余消除的第一优先目标。
但重复代码还有一个隐蔽变体:语义重复。多处实现用了不同的写法,解决的是同一个问题。
// A处实现 bool isAlive(const Player& p) { return p.hp > 0; } // B处实现 bool exists(const Player& p) { return p.hp != 0; }hp > 0和hp != 0在hp只有非负值时语义一样,但一旦hp可以变成负数,二者就会出现行为分岔。这种语义层面的重复才是最危险的,因为静态扫描工具很难发现它。处理方式不是简单二选一,而是抽成一个公共接口并明确其定义。
1.2 死代码与僵尸成员:隐藏的“包袱”
死代码指永远不会被任何入口触达的代码——未被调用的私有函数、无法到达的分支、未被读取的成员变量、空闲但未释放的资源池、被注释掉的整段实现。一位资深开发者跟我说过一句很直白的话:注释掉的代码不是文档,是负资产。它们让人误以为这块逻辑“还在起作用”,实际上只是占据阅读视野的僵尸。
提到一个真实案例:某项目底层有一个ConfigurationManager,提供四十多个公共方法,但真实运行路径上只用到其中八个。其余三十多个方法没有任何调用方,也没有测试覆盖。这类“半死亡”接口往往是因为历史版本为了兼容而保留,却再也没人接管。它们的存在会让新人在接需求时困惑:这个接口能改吗?会不会有人调用?排查下来又找不到调用点,白白消耗时间。
1.3 多余抽象与过度设计:聪明的“代价”
如果说重复代码是明面上的肥肉,多余抽象就是隐藏在皮肤下的水肿。为了“未来可能扩展”而提前做抽象,往往导致大量无业务含义的中间层。我看到过一个模块,底层就是一个配置读取,却套了IProvider接口、AbstractLoader基类、FileConfigAdapter适配器、CachedConfigProxy代理——四个类栈起来,真正的逻辑只有十几行。
这种结构的原始动机通常是“方便以后替换实现”,但现实是:接口从创立那天起就只有一个实现,一直没变过。C++的虚函数调用本身不是性能瓶颈,真正的问题是阅读成本和定位成本。为了追踪一个配置字段的读取路径,你得依次跳转四个文件。更麻烦的是,这种多层包装会诱发“耦合蔓延”——新需求为了绕过不合适的抽象,不得不往代理或适配器里继续塞逻辑,层数越叠越多。
1.4 冗余的开关、状态与分支:运行时的隐形消耗
有一类冗余不太被人注意,它们藏在代码的控制流里。
bool enableNewFeature = config->getBool("feature_x"); if (enableNewFeature) { doNewWay(); } else { doOldWay(); }当feature_x开关上线半年,新逻辑已经完全验证,旧分支早已无人触达时,这行if就是冗余。每个开关意味着至少两条执行路径、两份维护成本、以及翻倍的状态组合。在配置文件里保留一个永远为true的布尔开关,看起来无害,实则每次阅读都要思考“如果它为 false 会怎样”。状态压缩(删除永远成立的开关、合并等价状态)是开销最低且收益明显的简化方式。
1.5 冗余的包含与依赖:拖慢编译的元凶
C++的头文件展开机制决定了“无关包含”是编译性能的隐形杀手。一段代码只是用了std::string,却#include了整个自定义的GlobalInclude.h(该头文件又拉入了几十个第三方头文件);一个工具类只需要基础数据结构,却把整块网络库的接口都拽进来。这种依赖冗余会让编译时间随文件数量呈指数级恶化。
另一个常见场景是循环包含引起的不必要耦合。A需要B的前置声明,实际上用class B;就够了,却把B的完整头文件#include进来;一旦B修改了内部实现,A也要跟着重新编译。在某些大型重构中,一次仅优化 include 结构就能把全量编译时间降低三四成。这一点很多人会忽视,因为include不会产生运行时开销,但它消耗的是每天反复触碰的开发时间。
1.6 冗余的接口与包装层:披着“规范”外衣的转发
接口冗余和抽象冗余往往相互伴随。一个函数只是原样转发给另一个函数,没有任何额外的行为增强、参数处理或安全检查,那它就是纯粹的“传声筒”。
int getCount() { return getTotalCount(); } void clearData() { clear(); }这类转发如果只出现一两处还好,但当真出现几十个的时候,会让调用者产生错觉:这层接口本身代表某种边界或者语义约束。而实际上,绕过它直接调用底层函数,行为完全一致。这类冗余接口的可怕之处在于:它们让“公共API面”变得很宽,而宽的API面会显著增加文档成本、测试成本与契约维护成本。简化方式不是一次性删除所有转发层,而是逐层内联,把真正有叠加语义的逻辑留在外面,把空壳函数直接折叠。
1.7 冗余的运行时信息与资源:最后一块容易被忽略的短板
严格意义上这不完全属于“代码”冗余,但C++项目清理中它和代码冗余密不可分:为每个对象保存从未被读取的成员变量、为每次操作记录从未被查询的运行日志、为每个请求构建从未被观察的测量指标。这类冗余虽然不致命,却会占用缓存带宽、增加 allocated size、拖慢路径。
处理这类问题时,我通常建议团队先问三个问题:这个变量的值有没有被读过?这个日志有没有在监控面板上出现过?这个统计数据有没有对应的消费者?如果答案都是“没有”,那就应该进入降级或删除流程。
2. 动刀之前:识别、确认与优先级排序
冗余消除最先要解决的不是“怎么删”,而是“确认该删哪个”。这个话题聊起来容易,做起来却有门槛。我见过不止一个团队,因为“清理冗余”过于激进,把核心路径上承担隐式职责的代码删没了,线上出事故后回滚,然后整整半年没人再敢碰重构。动刀之前,必须先建立一套优先级和确认机制。
2.1 先分清楚“冗余”与“必要冗余”
并非所有冗余都该消除。有些冗余是防御性的,例如日志重试机制里的“重复状态检查”;有些冗余是沟通性的,例如公共API上的显式参数校验;有些冗余是性能性的,例如热路径上的局部缓存。这些都属于“必要冗余”——它们成本换来的是稳定性、可读性或性能收益。
一个标准配置文件的解析函数,在进入表情解析前先判定表达式是否为空,虽然上层调用点已经保证非空,但这个“多余”的判定能从根上避免后续任何人都可能引入的空指针问题。这种防御性代码在我看来就该留。识别“必要冗余”的标准其实很简单:删除它是否会导致行为边界、安全属性、可维护性出现明显退化。如果会,那就保留,并且最好在代码注释里说明“这里是故意重复”。
相反,需要消除的冗余是那种“留着没收益、删了不影响”的代码。所以严格来说,冗余消除的目标不是“零冗余”,而是“零无收益冗余”。
2.2 三层判断法:编译期、运行期、语义层
我的实践里,判断某段代码是否冗余,按三层递进确认:
第一层是编译期可见性。这个函数或类型是否出现在已编译产物的符号表里?用nm -C或objdump查看,一个符号没有任何引用方,基本可以认定是死代码。不过要注意:动态库里的导出符号、通过字符串拼接访问的符号、以及通过函数指针间接调用的符号,不能用简单的“无外部引用”来判死刑。
第二层是运行期可达性。代码是否被主流程创建的任何外部事件所触发?这一类判断通常需要辅助工具(gcov、静态分析),但更常见的办法是埋点观察:在被疑似冗余的路径入口处加一个计数日志,跑真实流量一周,监控计数是否为零。这个方案朴素但异常可靠,比任何静态推理都更能说明问题。
第三层是语义层等价性。多个实现是否在逻辑上重叠?这里的判断最需要领域知识。比如A模块里的GetUserName()和 B模块里的GetMemberDisplayName()看似返回同一个东西,但前者可能包含隐藏的权限过滤逻辑,后者没有。语义层冗余的判断标准不只是“代码长得像”,而是“在任何输入下行为都一致”。只要存在任何一个场景下二者结果不同,它们就不是冗余。
2.3 哪些代码不该动:红牌清单
下面这份清单是我在多个项目中验证过的“禁删区”,触碰任何一条都需要额外评审:
- 公共库和跨模块稳定接口:外部组件依赖它们进行链接或序列化。表面上找不到调用点,但脱离仓库的构建系统可能隐藏着引用。
- 序列化格式相关的代码和字段:即使当前无新代码写入,历史数据仍可能需要读取。删掉旧字段定义等于丢掉了兼容能力。
- 构造函数里的初始化动作:尤其是
static对象、全局单例和具有资源注册逻辑的类型。它们往往隐式地被某些“顺序敏感”的初始化过程依赖。 - 分支保护条件:如安全检查、降级开关、限流兜底。启动特征开关关闭时可以保留零位代码,但核心保护逻辑不属于冗余。
- 被“注释标注为故意保留”的代码:当维护者明确写了
// do not remove, used by legacy products之类的注释时,普通清理者没有权限推翻,需要产品所有者确认。
建立红牌清单的目的不是阻碍清理,而是让团队在快速清理时有一个可查的边界,减少误删导致事故的概率。
3. 手工重构的核心手法与实操步骤
工具可以帮助标记和度量,但真正高质量地消除冗余,靠的还是手工重构的基本功。这一节,用尽量具体的代码示例,把几个最常用的手法完整走一遍。
3.1 重复逻辑抽取:先找变体再提纯
重复逻辑的消除不是“看到两段相同的代码就直接抽函数”这么简单。直接用函数替换重复代码,如果参数过多、返回值表达混乱,反而会让代码更丑。我的建议是先找出重复块中的变体变量和不变骨架。
// 重构前:两个接口各自实现了相似逻辑 void SaveToJson(const Player& player, JsonDoc* doc) { (*doc)["name"] = player.name; (*doc)["level"] = player.level; (*doc)["hp"] = player.hp; } void SaveToXml(const Player& player, XmlWriter* writer) { writer->writeString("name", player.name); writer->writeString("level", player.level); writer->writeString("hp", player.hp); }这段代码真正的重复点是“把Player的核心字段依次序列化”这件事。重构方向不是把JSON和XML两套写法硬揉在一起,而是把“字段枚举”和“输出媒介”解耦。
// 重构后:核心逻辑统一遍历字段,输出媒介由回调决定 void ForEachPlayerField(const Player& player, std::function<void(const char*, const FieldValue&)> sink) { sink("name", player.name); sink("level", player.level); sink("hp", player.hp); }抽函数要遵守的原则是:抽取出来的函数必须能用一个清晰的名字概括职责,穷举不出来名字,说明抽象提取得不对。另外要注意的是,抽取过程不要夹带“顺手优化”。有一次我很顺手把一个重复块抽取后的循环改成用迭代器,结果引入了越界隐患,查了半天。重构时只做“结构等价变换”,这是规矩。
3.2 消除多余抽象:把中间层“摊平”
多余抽象的消除是最需要胆魄的环节。它的核心操作是逐层内联,而不是一次性“咔嚓”。
来看一个漂亮的例子。一个简单的配置读取,按照我的经验是:先把代理层的CachedConfigProxy内联掉,因为它只做了一层转发,没有真正的缓存逻辑;再把FileConfigAdapter的方法内容直接搬到AbstractLoader的默认实现里,把动态分发变成静态调用;最后如果只有一种实现,把接口类也移除。这个过程每一步都能编译、测试、提交,每一步的行为都等价。
这里我想特别强调“行为等价”的验证方法。内联后对比两个东西:一是重构前后的单元测试是否全部通过;二是在边界条件下(空文件、非法值、并发读取)的异常路径是否一致。如果这两者都满足,可以自信地提交。
3.3 简化过度设计的模板与继承
模板滥用导致的冗余,表面上看不像是“冗余代码”——因为每个实例化都是独立生成的目标代码。但从维护视角看,同一份算法被强行特化成多个版本,产生的重复和分支同样属于冗余家族。
解决手法有两个:当模板参数不影响行为时,用普通函数加参数来代替;当模板的多个实例化在行为上共享同一个核心流程时,把公共流程下沉到非模板基类。第二个手法在C++里是经典建议:
template <typename T> class StreamProcessor : public StreamProcessorBase { public: void process(Buffer& input) override { preprocess(input); // 可供派生类覆盖 coreProcess(input); // 模板无关的公共逻辑 postprocess(input); } void preprocess(Buffer&) {} void postprocess(Buffer&) {} virtual void coreProcessImpl(Buffer&) = 0; };这样写的好处是,公共流程只存在一份,避免所有T都实例化出一模一样的process实现。做这种事情时,千万注意模板的“可见性边界”——如果派生类在头文件外使用,模板的成员函数会被多次实例化,这种改造才能真正生效。
3.4 稳妥的“小步重构”流程
恰如开车,重构最怕的是猛打方向盘。我给自己定过一套“小步重构”流程,当作操作底线。
- 建基线:确认当前分支上测试全部通过,拍下关键性能数据(编译时间、二进制大小、关键路径耗时)。
- 拆分目标:一次只处理一种类型的冗余,不要同时做“重复代码抽取”和“头文件清理”,两件事搅在一起出了问题很难定位。
- 最小改动:每轮改动控制在5~10个文件以内,每轮都必须能独立编译、通过测试。
- 频繁提交:每完成一个等价变换就commit一次。万一后一步发现问题,可以直接revert到上一个提交点,而不是在五花八门的改动里找头绪。
- 双人复审:最关键的一步,至少让对这块代码较熟悉的同事过一遍。自己写的清理很容易陷入“我以为我知道这段代码做了什么”的盲区,旁人的视角能挖出不少隐式依赖。
遵循这套流程,我做过一次还算大型的冗余清理:涉及两个核心模块,前后三周,分了二十多个提交,没有一次回滚事故。背后不是运气,而是每一步都足够小、足够稳。
4. 静态分析工具实测手记与配置参考
手工审视决定“该不该删”,静态分析工具回答“哪里有嫌疑”。两者配合,效率最高。下面聊几款我在实际项目中用过的工具,按使用门槛排个序。
4.1 编译器告警:零成本的第一道关卡
很多冗余其实编译器一开始就能告诉我们,只是没人认真看告警输出。GCC和Clang打开这几个开关:
-Wall -Wextra -Wshadow -Wunused-variable -Wunused-function -Wunused-parameter -Wunused-but-set-variableMSVC 对应打开/W4。这些开关能直接捕捉:未被使用的局部变量、未被调用的静态函数、声明但未读取的成员变量(部分场景)、被隐藏的变量等等。
这里我要泼一个冷水:编译器的“未使用”不直接等于“可删除”。在头文件里定义的类内联函数,即使当前编译单元未调用它,编译器也不会报警,因为无法判断其他编译单元是否使用了它。要找出跨编译单元的死代码,必须依赖链接阶段的“垃圾回收”机制。
Linux下链接时使用-ffunction-sections -fdata-sections -Wl,--gc-sections,可以把未引用的函数和数据剔除出最终二进制。这个方法同时也就暴露了哪些函数没人引用——剔除后观察符号表即可。但这套方案只有配合链接优化才有效,调试版本的符号保留会让判断失真。
4.2 开源静态分析工具的实用配置
在编译器告警之上,推荐 Clang-Tidy,它是Clang生态里针对“冗余”最细腻的工具之一。常用的一组检查项是:
clang-tidy -checks="-*, readability-redundant-*, misc-unused-*, performance-*"其中readability-redundant-*涵盖了:
readability-redundant-string-cstrreadability-redundant-member-initreadability-redundant-control-flowreadability-redundant-declaration
misc-unused-*能处理using namespace声明、using 别名、静态函数等未用符号。
另一款老牌工具 Cppcheck 针对死代码和冗余分支有一个实用选项:
cppcheck --enable=all --inconclusive --check-level=exhaustive --suppress=missingIncludeSystem .实测下来,这两款工具各有分工:Clang-Tidy 对 C++ 新特性的精确度高,Cppcheck 对跨函数状态的误报率更低。不过两家的共同特点是:在模板代码上都会漏报。模板未被实例化时,很多检查项无法进行语义分析,工具只能静默跳过。这不能怪工具,而是必须有意识地用手工方式补上模板区域的检查。
4.3 误报处理与结果排序
任何一个工具都会产生误报,关键是要建立一个“汇报—筛选—反馈”的循环。我个人的做法是:第一次跑全量扫描时,把所有报警导出到一个报告文件。三名团队成员各自独立标记“确定”“存疑”“误报”三个等级,再开会讨论。这个过程结束后,把明确是误报的检项加入配置文件的白名单,并注明原因。
在实际处理报警时,建议按以下优先级排序:
- 高优先级:
misc-unused-*且可确认无外部引用;readability-redundant-control-flow的尾部return。 - 中优先级:模式重复类报警(两个分支结构完全相同、多个条件表达式相同);未使用的成员变量。
- 低优先级:
performance-*类;命名规范类冗余。
把可自动修复的报警排在一起批量处理,能极大提高效率。比如clang-tidy -fix可以把字符串函数冗余调用自动改写,完全不需要人工逐行替换。但这里必须提醒:-fix只负责改写,不负责任何语义验证。改完必须全量跑单测(unit tests)和回归测试,尤其要关注失败用例提示的越界隐患。
5. 量化收益:用数字说服自己和团队
代码冗余消除最大的隐性风险,是“做完了说不清楚收益”。团队leader看到一堆提交记录,却不知道这些改动到底带来了什么。没有量化结果,下轮重构的预算就很难批下来。建议从四个方面采集数字。
5.1 建立基线数据
动手之前,把项目快照存起来,然后记录四项指标:
| 指标 | 测量方法 | 工具参考 |
|---|---|---|
| 全量编译时间 | 清空构建目录后完整编译一次 | time ninja/time make -j8 |
| 二进制体积 | 测量链接产物大小(剔除调试信息) | size/nm --size-sort |
| 静态代码规模 | 按源码文件统计有效代码行 | cloc |
| 圈复杂度均值 | 统计每个函数的McCabe复杂度 | cppstats/radon(C++需转格式) |
另外还应该记录一组“体验指标”:新人上手时从读代码到定位到首个功能改动点的耗时、常规需求评审时间、上一次模块维护的Bug数量。这些听起来很抽象,却是冗余最真实的口径——冗余的最终代价,是每个维护者每天都在它身上浪费的几分钟。
5.2 消除效果的对比方法
消除阶段结束后,用同样方法重新测量,形成对照表。
我曾有一个中等规模的项目,做了一轮以“消除死代码+多余抽象”为主的清理,结果如下:
- 全量编译时间从14分钟降到9分钟,降幅约36%。
- 二进制体积从188MB降到161MB,少了约27MB。
- 静态代码行数减少约15%,头文件依赖层级减少两层。
这个结果最惊喜的是编译时间。原因在于:死代码虽然不会参与链接,但会在编译阶段进行语义分析、类型检查、模板实例化,每一行代码都在消耗编译器资源。移除后,编译器负载明显降低。这里尤其要注意的细节是:二进制体积的降幅一般小于代码行数降幅,因为它只剔除了“链接器认为不可达”的代码段,还有一部分“未用但被导出”的符号会残留在动态库里,需要通过符号可见性设置才能进一步压缩。
5.3 收益滞后的心理预期
量化收益还有一个必须提前做好的心理建设:一部分收益会滞后显现,而不是立刻体现在表格里。比如代码结构变清晰,短时限内不会让程序跑得快,但会减少新需求的开发周期;依赖精简,不会缩减运行速度,却会让增量编译更快,开发者每天省下十来分钟。
团队里如果有人只看“线上延迟有没有变化”,那大概率会失望。正确的方式是把“冗余额外成本”换算成“维护工时”,例如代码复杂度降低后,评审时间压缩、缺陷率下降。这类数据需要积累两三个迭代周期才能体现,不能操之过急。
6. 常见问题与排查技巧实录
这个部分想分享一下我在使用中实际碰到的问题与解法,也整理一份速查表,方便各位在实际排查时参考。
6.1 改错行为类问题:审慎验证三种场景
在冗余消除中,“行为等价”验证失败时的问题通常集中在三个场景:
场景一是删除“看起来没被读”的成员变量,却改变了对象内存布局。在C++里,成员变量的删除会改变sizeof(T),进而影响容器的内存分配和拷贝行为。如果项目里有对齐相关的#pragma pack约定,或者把对象直接memcpy到缓冲区,这种改动会引发不可预知的运行结果。排查方法是:删除成员后,检查是否有代码直接使用sizeof(T)或依赖对象大小的序列化/反序列化逻辑。
场景二是消除“从未被调用的私有函数”,却断送了未来的安全兜底。比如一个日志采集模块的降级函数,虽然没有被显式调用,但可能被外部监控脚本通过dlsym动态查找。删除前必须做全仓库的字符串搜索,排查是否存在符号查找场景。
场景三是简化“冗余的开头检查”后,引入了空指针风险。前面说过,不必要的防御性检查可能被合理移除,但要确认所有调用点都确实保证了前置条件。在一个上线项目里放过“顺手删掉访问函数前的空指针判断”的错,结果平时启动路径没问题,但某次配置文件加载异常时,上层传入空指针,直接段错误。从那以后,凡是要删防御性判断,我都会先重新审查所有调用链。
6.2 工具与流程中的常见坑
冗余消除执行中,工具和流程本身也会带来一些经典陷阱。
表格梳理如下:
| 现象 | 根因 | 排查与对策 |
|---|---|---|
clang-tidy -fix修改了A文件,联动改了B文件 | -fix默认跨越编译单元做修整 | 在修改后重启一次全量构建,逐一对比差异 |
| 剔除模板实例化后,运行期性能反而下降 | 删除的是某个“意外有效”的模板专用化 | 用profiler对比性能热点;必要时恢复专用化 |
| 编译时间没降,甚至略有上升 | 头文件清理同时删除了关键前置声明,导致某些文件重复展开 | 检查include覆盖范围;冷暖分区重新整理头文件 |
| 源码行数减少明显,但二进制不变 | 导出的公共符号阻止链接器回收 | 调整符号可见性,配合版本脚本导出白名单 |
| 静态分析报告的中高优先级项全是有意保留 | 项目缺少“必要冗余”注释规范 | 在代码或工具配置里标记NOLINT并写明原因 |
| 核心路径的“防御性代码”被误判为冗余 | 静态工具无法理解领域职责 | 将核心保护逻辑与普通逻辑在目录结构上分开,工具配置里按路径规避 |
矩阵里的第六项尤其常见。要处理它,与其和工具斗争,不如在代码中把“故意保留”的冗余用规范注释标出来。这样无论是同事做代码评审,还是将来工具扫描,都能一眼分辨“必要冗余”和“无收益冗余”。
6.3 团队协作时的实用建议
最后,我坚持认为:冗余消除这件事,很大程度上是“团队习惯”问题,而非单纯的技术问题。一个人在夜里推一个庞大的重构分支,哪怕逻辑完全正确,评审者也会因为diff量太大而不愿意仔细看,最终不了了之。把重构拆成小的、可独立审查的提交,配合上面的静态分析报告,每个提交只解决一个明确问题,复审者才愿意投入精力。
另一个有用的做法是:在团队的评审规范中,明确要求“新增代码不得制造已知冗余”。这一步做完,后续清理的压力会小很多。比如:
- 新增函数前,先搜索是否已存在相同语义的工具函数;
- 新增成员变量前,明确“会被谁读取”的答案;
- 新增开关前,注明“该开关的生命周期和下线条件”;
- 新增无效析构函数、空构造函数、以及与默认行为完全一致的容器拷贝时,尽量直接省掉。
我见过不少团队的冗余增长,不是因为某一次大动作写坏了,而是因为“每人每天加一点”的惯性。恰恰是这些不起眼的小冗余,在几年里堆出一个维护成本极高的代码库。如果代码评审阶段能从源头上拦住一部分,后面做专项清理时也会轻松得多。
按我个人经验,每次专项清理做完后,我都会留出半小时,把本次的“误删风险点”复盘成清单,放进团队的文档里。下一次再做类似清理,所有人都能更快定位高风险区域。这样的积累会让冗余消除从一次性的“大扫除”,变成可持续的日常习惯。