本文要点
- 代码冗余的原因多种多样,从未使用的变量到未完成的变更,再到废弃的代码;
- 冗余代码会产生一系列的影响,包括源代码臃肿、可靠性及可维护性降低。在某些情况下,死代码也会影响性能;
- 为了检测冗余代码,作者开发了一个工具,使用 Roslyn 创建 C#源码抽象语法树。作者使用包括 Roslyn 和 MSBuild 在内的多个 GitHub 项目对这个工具进行了训练;
- 检测到冗余代码可以手动删除或添加注释,也可以使用一种自动化工具。使用特性分支有助于防止冗余代码检入主代码分支。
导言
前段时间,我开发了一款工具,分析源代码中的依赖关系。它使用Roslyn 创建C#源代码抽象语法树,使用libclang 创建C++ 源代码抽象语法树。为了验证它是否可以取得预期效果,我接下来实现了识别未使用方法的功能。结果显示,C#代码解析比C++ 代码解析准确得多,因此,我选择把重点放在C#分析器的进一步开发和其他人开发的更复杂的C#代码上。
起初,该工具会标记出冗余方法所在的行,在弄清楚问题范围之后,我实现了自动删除那些行的选项。一个典型的分析过程会多次执行这个工具,尽可能地修剪源代码树。接下来是多个变更还原循环,以便可以成功地构建并通过测试。失败的原因是工具行为异常或者已知的局限性,例如,反射或代码契约。
我选择了多个自己用过而又想回馈的C#项目,用它们的GitHub 库训练了这个工具。最后,我向社区提交了pull request,请求他们讨论我在自己的分支里做的变更。由于这个工具很苛刻,而我又是第一次在网上与人交流,不懂技巧,所以希望我没有冒犯太多的人。在向社区做贡献及参与后续讨论的过程中,我对问题的理解更深入了,本文旨在将我的所得回馈给更广泛的社区。
分析过的GitHub 项目
由JB Evain 编写的 Mono.Cecil 可以将.NET 代码反编译成 C#。根据建议,只有 36 行代码需要删除,经过审核,JB 选择单独添加部分变更,而不是合并分支。
Automatic Graph Layout 是微软官方的一个项目,由 Lev Nachmanson、Sergey Pupyrev、Tim Dwyer、Ted Hart 和 Roman Prutkin 开发,用于绘制图和有向图,Visual Studio 也用它显示各种交互图。Pull request 要求删除 4674 行代码,其中有一些和 SilverLight 有关(已于 2015 年宣布停用)。不经过修改或讨论,分支就被合并了进去。
Roslyn 是一个现代化的 C#编译器,由.NET 基金会的一个团队负责维护。在这个例子下,Pull request 要求删除 18364 行代码,这引发了有益的讨论,并产生了下面讨论的大多数分类。显然,这个分支太大了,无法合并,取而代之,多个单独的议题被提了出来。
MSBuild 是微软官方的一个项目,Visual Studio 的用户应该比较熟悉。根据分析,我提交了删除 3722 行代码的 pull request,遗憾的是,其团队当时没有余力审核我提出的变更建议。
最后分析的是.NET Core 基础库里的System.XML 程序集。这些库由.NET 基金会负责维护,为了删除死代码,其团队发布了一条问题追踪信息。该问题的解决方法是逐个修剪程序集(通常被称为死代码消除),然后比较未修剪程序集和已修剪程序集之间的差异,从而确定哪些编译代码被删除了。通过这些差异可以知道哪些源代码被删除了,这项工作通常是由志愿者社区承担。
鉴于只用该工具抽样分析了五个项目,得出任何结论都是不明智的,尤其是在缺少可靠的统计数据的情况下:
Mono.Cecil MSAGL Roslyn MSBuild CoreFX (System.XML) 已删除 36 4674 18364 3722 427 初次提交时间 2010/04/12 2015/02/22 2014/03/18 2015/03/12 2014/11/08 作者(主要) 39 (1) 25 (4) 285 (31) 90 (6) 526 (29) 鉴于其悠久的历史,MSBuild 的初次提交时间值得注意。仅数一下作者的数量而不评估他们的贡献几乎可以肯定是没有意义的,因此我大致估计了主要的贡献者。说到这里,我推测:
- 新开发的项目比成熟项目冗余代码多
- 编写代码时假定将来会需要
- 测试少,相应的,发现的 Bug 就少
- 团队越大制造的冗余代码越多
- 沟通成本会随团队规模的扩大而呈几何级数上升,参见《人月神话》
测试结果分类
该工具测试冗余方法,和任何测试一样,分析有成功和失败之分。
真负:
- 代码有用
假负:
- 测试代码
- 死代码
真正:
- 故意放弃的开发
- 方法已经添加,但环境变了,这些方法不会再使用了
- 未完成的变更
- 代码已变更,有些方法所有引用都删除了,但后续提交时没有将其考虑在内。这种代码有时候称为 oxbow 代码。它可能是重构的意外产物。
- 未使用的变量
- 但愿 IDE 会生成编译器警告和 / 或报告。
假正:
- 工具缺陷
- 回归
- 在开发过程中,经过修改,一个方法不再调用标识为死亡的方法,此时,这种情况就会出现。显然,没有测试识别这种回归。
- 缺少条件编译
- 通常,这是指缺少#if DEBUG 保护。Roslyn 已经用于 Release 构建,这个过程会识别相应二进制文件中的代码膨胀。
- 无意放弃的开发
- 这是在特定的测试数据下发现的,但没有相应的测试。通常来说,我希望这是一个暂时的问题,随着代码增加会自然消失。
- 在子系统中未使用
- 分析低效的代码,找出使用该方法的地方。虽然反射也会导致这个问题,但这主要出现在公共方法分析时。一个常见的例子是执行测试辅助功能的时候没有相应的测试代码,不过,那会抛出一个设计问题,定位到测试辅助代码。
- 仅限调试器使用的方法
- 在附加了调试器时(可能是 Release 构建),有些项目会有方法打印出状态。这些方法应该以某种方式标记出来,那样就不用分析它们了。
需要注意,工具失败不一定是因为推理不足。其中有些情况是其他工具的处理范畴,如编译器警告应该标记部分问题,重复代码检测器也有用。
冗余代码的影响
假负和真正都可以视为 YAGNI 的实例,这就需要我们注意以下事项:
-
- 以前,我遇到过文本编辑器无法打开大文件的情况。
- 开发人员的认知受限于大脑中可以同时容纳的独立实体的数量,通常认为是 7 个,因此,删除方法可以提升推理能力。
- 所有开发人员用来阅读和理解源代码的时间很可能都是浪费。
- 现代 IDE 通常都是从源代码生成抽象语法树,因此,冗余代码降低了它们的速度。
-
臃肿的可执行文件
- 在手机上,这尤其是个问题,为了释放存储,升级会删除应用,最终,当功能受到过度损害,就不得不升级设备了。
-
臃肿的运行时
- 为黑客提供了更大的攻击面,让他们发现安全问题。
- 部分面向嵌入式设备的应用程序会在启动时分配所有的动态内存,因此,冗余代码会减少可用堆的大小。
-
降低性能
- 在特定的情况下,死代码执行会浪费处理器周期。
-
降低可靠性
- 在特定的情况下,死代码执行可能导致崩溃。
-
降低可维护性
- 如果执行到冗余代码时停下了,那么行为将不可预测,如 Knight Capital 损失了 4.4 亿美元。
- 代码覆盖率可能会因假负而升高,因假正而降低。
- 不必要的测试会降低测试套件的执行速度。
- 静态分析工具不得不处理更多的代码,并因此效率降低。
- 软件的设计 / 架构看起来比实际情况复杂。考虑到冗余代码会导致各种各样的问题,最好是将其视为代码异味。
管理已有的冗余代码
处理冗余代码有四种方法:
-
忽略
- 虽然会增加维护成本,但至少可以通过编译。
- 不推荐。
-
自动从可执行文件中移除
- 编译型语言在链接器中使用任意一种死代码移除选项移除。
- 动态语言使用 Tree Shaking 在运行时移除。
- 也不推荐。
-
屏蔽
- 注释掉或者使用预编译指令。
- 使用 IDE 提供的代码 / 注释折叠避免看到它。
-
删除
- 借助源代码控制保留旧版本供参考,可以随时查看。
- 或者你可能永远不再需要它。
如果要变更源代码,任何变更都需要由熟悉这段代码的人审核,务必保证代码真冗余。
管理新开发项目
假如目标是在开发过程中不引入新的冗余代码,需要制定什么策略?如果允许部分提交,代码库中可能就会增加暂时冗余的代码。这种情况可以通过特性分支来缓解,只有当测试已覆盖且源代码通过了静态分析才合并进主分支。特性分支的另外一个好处就是在放弃开发后可以将分支保持在未合并状态。
关于作者
Zebedee Mason 是一名有着 25 年 CAD/CAM/CAE 行业经验的数值分析员,他最近搬到了 SLAM。多年来,他一直从事与遗留代码库(其中有些在他还是个孩子的时候就已经创建)相关的工作,把许多学术代码改写成商业软件的组件,他甚至还重写了一些原始算法(商业秘密,无法确认)。这种背景使他对软件工具非常了解,他也编写了若干提高生产力的工具,即使以前的开发人员和 IT 管理人员做出了有用的选择。
评论