死代码是指程序中永远不会执行且不影响结果的代码片段,但它们物理上仍然保留在项目的源代码中。与被注释的部分不同,死代码会被编译并进入二进制文件,增加其大小并增加导航难度。根据TIOBE Index (2025)的研究,平均商业项目包含10%到25%从未被调用的代码。僵尸代码是死代码的一个子类型,在过去曾经运行过,但在重构后失去相关性,现在只占用空间。定期清理这些片段可以降低开发人员的认知负荷,并减少在进行更改时出现错误的风险。
要点
死代码(dead code)是指包含在程序中但在任何使用场景中都不会执行的源代码。编译器或解释器会处理它,但在运行时,控制流永远不会进入这些部分。
死代码的经典示例:被赋值但从未读取的变量;从未被调用的函数或方法;永远不会变为真的条件分支(if(false));循环体一次也不会执行的循环。
根据SonarQube State of Code Quality(2025)报告,商业Java项目中约15%的警告与未使用的私有方法和字段有关。在JavaScript项目中,由于语言的动态特性和大量第三方库,未使用代码的比例可能高达30%。
定期检查项目中是否存在死代码——尤其是在大型重构和删除功能之后。今天一个被遗忘的import或未使用的函数明天可能变成僵尸代码,误导新团队成员。
僵尸代码(zombie code)是死代码的一种特殊情况,其特点是具有历史背景。僵尸代码曾经运行过,但在系统变更后变得不可达,尽管它没有被删除,而是被保留下来“以防万一”。
死代码和僵尸代码之间的区别在于起源。死代码可能是错误编写的(从未运行过),而僵尸代码是曾经活跃的代码,在重构过程中失去了相关性。例如,基于旧业务逻辑的折扣计算函数,该逻辑已被新逻辑取代,但旧方法未被删除——以备需要恢复时使用。
僵尸代码的主要危险在于它给人一种功能仍在工作的错觉。新开发人员看到一个带有文档的函数,假设它在某个地方被调用——然后花时间研究一个人造物。当尝试直接调用它时,可能会发现它依赖于已删除的实体或过时的API。
通过git历史跟踪僵尸代码:如果一个函数两年来没有变化并且没有被使用——它就是僵尸。毫不犹豫地删除它,因为git保存了历史记录,必要时代码总是可以恢复。
第一个也是最常见的原因——迭代开发加上不完整的重构。团队添加替换旧功能的新功能,但不删除被替换的模块。冲刺累积这样的“尾巴”,一年后项目就被死代码层覆盖。
第二个原因——A/B测试和功能开关。新功能的启用条件会随着时间的推移而固定下来(例如始终为true),但带有替代逻辑的else分支仍保留在代码中。开发人员害怕删除它,以免在开关被重新切换时意外破坏系统。
第三个原因——自动生成和复制粘贴。代码生成器(IDE、模板引擎)创建带有开发人员未填充或未使用的方法的模板。从另一个项目复制的代码通常包含与新上下文无关的整个代码块。
第四个原因——害怕删除。在大型项目中,开发人员害怕删除代码,因为他们不确定它是否真的在任何地方被使用。这种恐惧因测试系统薄弱而加剧:如果没有自动检查,删除可能会导致仅在生产环境中才发现的错误。
死代码直接影响项目质量的四个方面:编译性能、产物大小、团队的认知负荷和重构的可靠性。
编译时间增加:编译器处理未使用的文件,分析依赖关系,并为永远不会运行的片段生成字节码或机器码。在大型项目中,这会给每次编译增加几分钟。对于解释型语言(JavaScript、Python),模块加载时间和内存消耗都会增加。
修改时的错误风险:开发人员在修改代码时,不知道函数只在死分支中使用。重构后,死代码无法编译或产生错误——团队花时间诊断一个不影响应用程序运行的问题。
认知负荷是最昂贵的因素。每个未使用的函数在阅读代码时都需要注意。开发人员花费脑力去理解这段代码为什么存在以及在何处被调用。Developer Productivity Lab(2025)的研究表明:删除20%的死代码可使入职时间平均减少18%。
在发现死代码后立即删除。每一天的延迟都会增加团队中有人花费数小时研究本应昨天就删除的人造物的可能性。
死代码的搜索通过两种主要方法进行:静态分析(不运行程序)和动态分析(运行时覆盖率分析)。每种方法对不同类型死代码都有效。
静态分析器支持所有流行的编程语言。对于Java和Kotlin——SonarQube、IntelliJ IDEA Inspections、SpotBugs。对于JavaScript和TypeScript——ESLint配合no-unused-vars和no-unused-modules规则。对于Swift——SwiftLint配合unused_declaration规则。对于Python——pylint配合unused-import选项和vulture进行深度搜索。
// build.gradle.kts — Android 的 ProGuard 配置
android {
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
// proguard-rules.pro — 仅保留所需的类
-keep class com.example.app.** { *; }
-assumenosideeffects class Timber {
static <methods>;
}
ProGuard不仅删除未使用的类和方法,还会在release构建中压缩名称。启用ProGuard的构建会自动显示哪些类和方法被视为未使用——在usage.txt报告中列出了所有被删除的代码。
代码覆盖率工具(Java的JaCoCo、Swift的XCTest coverage、JavaScript的Istanbul)显示测试期间哪些行和分支被执行。覆盖率为零的方法是死代码的候选。然而,缺乏覆盖并不能保证代码在生产中不被调用——要完全确定,请结合使用静态和动态分析。
配置CI流水线,使得当未使用声明的阈值被超过时构建失败。SonarQube Quality Gate配合“未使用的私有代码比例不超过3%”的规则,可在开发过程层面防止死代码的累积。
死代码的删除过程包括四个步骤:找到、检查、删除、再次检查。跳过任何步骤都会增加回归风险。
第一步——通过静态分析器查找候选代码。获取有关未使用声明的报告:函数、类、变量、import。过滤误报——分析器有时会在反射、动态类加载或通过序列化的隐藏调用时出错。
第二步——通过git blame和更改历史进行检查。查看代码是何时以及为何编写的。如果代码是通过功能开关禁用的功能的一部分,请确保开关已固定且不会重新启用。注释掉您犹豫是否要删除的代码,并留下一个TODO任务,一个月后重新检查。
第三步——在单独的分支中删除并运行完整的测试套件。如果测试通过——回归可能性低。如果测试失败——意味着代码仍在使用,需要确定在哪个场景中。
// 之前 — 同一文件中的死代码和僵尸代码
int calculateV1(int price) { // 未在任何地方调用
int tax = price * 0.18;
return price + tax;
}
int calculateV2(int price, double rate) {
return static_cast<int>(price * (1 + rate));
}
// 之后 — 死代码已删除,僵尸代码已清理
int calculatePrice(int price, double rate) {
return static_cast<int>(price * (1 + rate));
}
第四步——对更改进行代码审查。审查者必须确认代码确实是死的。如果审查者不确定——在代码中留下注释,并将删除推迟到全面分析之后。分支合并后,删除该分支,以避免在git仓库中滋生僵尸代码。
实施一个规则:任何pull request都不应包含新的死代码。在pre-commit钩子中添加linter,当存在未使用的变量或import时阻止提交。预防总是比清理更便宜。
常见问题
是的,如果死代码包含语法错误或引用已删除的类型。现代编译器仍然检查死分支,因此if(false)块中的错误将导致构建被拒绝。这是一种保护:代码不应该死到编译器不检查它的程度。
僵尸代码具有误导性:新开发人员看到一个带有文档的函数,假设它被使用。他花时间研究不工作的代码,并可能意外地将新逻辑与过时的实体关联起来,从而产生难以追踪的错误。
使用ESLint配合no-unused-vars和no-unused-modules规则,以及knip工具——它分析整个项目中的exports和imports,查找未使用的文件、函数和依赖项。对于大型monorepo,knip提供最完整的视图。
最好在发布之前删除,但不要在最后一刻。删除死代码是一项技术性工作,应在冲刺中单独规划。就在发布之前,如果代码并不像看起来那样死,删除可能会导致不稳定。
是的,现代编译器和压缩工具(ProGuard、R8、Terser、Closure Compiler)在死代码消除层面上删除不可达的代码。然而,这并不能替代清理源代码的必要性:编译器从二进制文件中删除代码,但不会从仓库中删除——开发人员在阅读时仍然会碰到它。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。