C# 中的圈复杂度

Hacker News Top 新闻

摘要

本文解释了 C# 中的圈复杂度,涵盖其计算方法、阈值、度量标准以及用于改进代码质量的重构技术。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/19 00:30

# 理解圈复杂度 -- NDepend 来源:https://blog.ndepend.com/understanding-cyclomatic-complexity/ 2026年5月25日 阅读时长9分钟 C# 中的圈复杂度 (https://blog.ndepend.com/wp-content/uploads/Cyclomatic-Complexity-in-C.png) C# 中的 **圈复杂度**(或 **CC**)是一种代码度量指标,用于计算方法内**线性独立执行路径**的数量。具体来说,它计算为 1 加上方法体中的分支结构数量(例如 `if`、`while`、`for`、`case`、`&&`、`||`、`?:` 和 `??`)。分数越高,方法越难阅读、测试和安全修改。分数为 1 表示只有一条直线路径,10 左右是 Thomas McCabe 传统推荐的上限,超过 25 则被 Microsoft 的 CA1502 (https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1502) 分析器标记为过度复杂。本指南通过 C# 示例解释圈复杂度的计算方法、实践中重要的阈值、如何在真实 .NET 代码库中度量和可视化它,以及如何通过将原始分数与测试覆盖率和 IL 级分析相结合来超越其本身。 - [什么是圈复杂度?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#What_is_Cyclomatic_Complexity) - [C# 中圈复杂度的定义](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Definition_of_Cyclomatic_Complexity_in_C) - [C# 中圈复杂度影响的示例](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Example_of_Cyclomatic_Complexity_Impact_in_C) - [展示一个复杂方法](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Exhibiting_a_Complex_Method) - [将复杂方法重构为多个更简单的方法](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Refactoring_the_Complex_Method_in_Several_Simpler_Methods) - [重构的好处](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Benefits_of_Refactoring) - [圈复杂度阈值:多高的分数算过高?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Cyclomatic_Complexity_Thresholds_What_Score_Is_Too_High) - [在 C# 中度量圈复杂度](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Measuring_Cyclomatic_Complexity_in_C) - [规范 C# 圈复杂度](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Ruling_C_Cyclomatic_Complexity) - [可视化 C# 圈复杂度](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Visualizing_C_Cyclomatic_Complexity) - [C# 圈复杂度与测试](https://blog.ndepend.com/understanding-cyclomatic-complexity/#C_Cyclomatic_Complexity_and_Tests) - [超越圈复杂度](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Going_Beyond_Cyclomatic_Complexity) - [将复杂度与分支覆盖率配对](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Pair_Complexity_with_Branch_Coverage) - [第三方代码的 IL 圈复杂度](https://blog.ndepend.com/understanding-cyclomatic-complexity/#IL_Cyclomatic_Complexity_for_Third-Party_Code) - [如何在 C# 中降低圈复杂度](https://blog.ndepend.com/understanding-cyclomatic-complexity/#How_to_Reduce_Cyclomatic_Complexity_in_C) - [1. 提取方法](https://blog.ndepend.com/understanding-cyclomatic-complexity/#1_Extract_Method) - [2. 提前返回 / 保护子句](https://blog.ndepend.com/understanding-cyclomatic-complexity/#2_Early_Return_Guard_Clauses) - [3. 用多态替换条件](https://blog.ndepend.com/understanding-cyclomatic-complexity/#3_Replace_Conditionals_with_Polymorphism) - [4. 使用现代 C# 模式匹配和 Switch 表达式](https://blog.ndepend.com/understanding-cyclomatic-complexity/#4_Use_Modern_C_Pattern_Matching_and_Switch_Expressions) - [5. 为纯映射使用查找表](https://blog.ndepend.com/understanding-cyclomatic-complexity/#5_Use_Lookup_Tables_for_Pure_Mappings) - [6. 布尔参数是代码异味](https://blog.ndepend.com/understanding-cyclomatic-complexity/#6_Boolean_Parameters_Are_a_Code_Smell) - [常见问题](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Frequently_Asked_Questions) - [C# 中好的圈复杂度分数是多少?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#What_is_a_good_Cyclomatic_Complexity_score_in_C) - [else 关键字会增加圈复杂度吗?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Does_the_else_keyword_increase_Cyclomatic_Complexity) - [switch 语句是计算一次还是每个 case 计算一次?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Does_a_switch_statement_count_once_or_per_case) - [圈复杂度是否等于我需要编写的单元测试数量?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Does_Cyclomatic_Complexity_equal_the_number_of_unit_tests_I_need) - [圈复杂度与认知复杂度有什么区别?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#What_is_the_difference_between_Cyclomatic_Complexity_and_Cognitive_Complexity) - [如何在 Visual Studio 中度量圈复杂度?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#How_do_I_measure_Cyclomatic_Complexity_in_Visual_Studio) - [圈复杂度对异步或 LINQ 代码有效吗?](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Does_Cyclomatic_Complexity_work_on_async_or_LINQ_code) - [结论](https://blog.ndepend.com/understanding-cyclomatic-complexity/#Conclusion) 圈复杂度由 Thomas J. McCabe 在 1976 年提出,旨在量化一段代码的结构复杂度。该概念源于图论:每个方法都可以表示为一个*控制流图*,其中节点是语句块,边是它们之间的跳转。在该图上,圈复杂度由经典公式给出:其中 *E* 是边的数量,*N* 是节点的数量,*P* 是连通分量的数量。对于具有单一入口和单一出口的常规方法,这简化为 **1 + 决策点的数量**,这也是大多数工具实际计算的形式。这个数字真正告诉你的是,需要多少个测试用例才能执行通过该方法的每条独立路径。这就是为什么圈复杂度能持续近半个世纪的原因:它是一个结构指标,但对于每个必须维护或测试代码的人来说,它具有非常具体的可操作意义。 ## C# 中圈复杂度的定义 C# 方法 (https://www.ndepend.com/docs/code-metrics#CC) 的圈复杂度具体为 1 + {在方法体中发现的以下表达式的数量}:`if` `while` `for` `foreach` `case` `default` `continue` `goto` `&&` `||` `catch` 三元运算符 `?:` `??` `and` `or` 以下表达式**不计入** CC 计算:`else` `do` `switch` `try` `using` `throw` `finally` `return` 对象创建 方法调用 字段访问 有两个细节容易让人困惑:`else` 不会增加分数,因为其对应的 `if` 已经创建了替代路径;而 `switch` 每个 `case`(以及 `default`)贡献一个单位,而不是 `switch` 关键字本身贡献一个。C# 模式匹配构造(`and`、`or`,以及使用模式的现代 switch 表达式)也以与它们传统对应物相同的方式增加分数。 ## C# 中圈复杂度影响的示例 ### 展示一个复杂方法 这是一个包含交织的 `if` 和 `else` 作用域的复杂方法。`if` 关键字使用了六次,`&&` 使用了一次。因此其圈复杂度分数为 8: ```csharp public static class OrderLogic { public static void ProcessOrder( int orderId, bool isPriority, bool isInternational, bool isGift, bool isCouponApplied, decimal orderTotal) { if (orderId <= 0) { Console.WriteLine("Invalid order ID."); return; } if (isPriority) { Console.WriteLine("Processing priority order."); if (isInternational) { Console.WriteLine("Processing international priority order."); if (isGift) { Console.WriteLine("This is a gift order."); } } } else { Console.WriteLine("Processing standard order."); if (isInternational) { Console.WriteLine("Processing international standard order."); } } if (isCouponApplied && orderTotal > 100) { Console.WriteLine("Applying discount for orders over $100."); } else { Console.WriteLine("No discount applicable."); } } } ``` 八条独立路径意味着至少需要八个测试才能完全覆盖这单个方法,而且每次有人需要添加新业务规则时,还要花费大量精力去思考。这正是那种容易悄无声息地引入回归错误的方法。 ### 将复杂方法重构为多个更简单的方法 上面的方法可以重构为几个复杂度较低的方法。在代码中,我们使用 CC 来表示每个方法的圈复杂度分数: ```csharp public static class OrderLogic { public static void ProcessOrder( int orderId, bool isPriority, bool isInternational, bool isGift, bool isCouponApplied, decimal orderTotal) { // CC 2 if (!IsValidOrder(orderId)) return; ProcessOrderType(isPriority, isInternational, isGift); ApplyDiscountIfEligible(isCouponApplied, orderTotal); } private static bool IsValidOrder(int orderId) { // CC 2 if (orderId <= 0) { Console.WriteLine("Invalid order ID."); return false; } return true; } private static void ProcessOrderType( bool isPriority, bool isInternational, bool isGift) { // CC 5 if (isPriority) { Console.WriteLine("Processing priority order."); if (isInternational) { Console.WriteLine("Processing international priority order."); } } else { Console.WriteLine("Processing standard order."); if (isInternational) { Console.WriteLine("Processing international standard order."); } } if (isGift) { Console.WriteLine("This is a gift order."); } } private static void ApplyDiscountIfEligible( // CC 3 bool isCouponApplied, decimal orderTotal) { if (isCouponApplied && orderTotal > 100) { Console.WriteLine("Applying discount for orders over $100."); } else { Console.WriteLine("No discount applicable."); } } } ``` ### 重构的好处 - **更简单的控制流:** 主方法现在将特定任务委托给更小、更专注的方法。 - **更易于测试:** 你可以独立地测试每个小方法。 - **更低的圈复杂度:** 复杂度分散到多个方法中,使得每个方法更容易独立理解和维护。 - **更好的命名:** 像 `IsValidOrder` 或 `ApplyDiscountIfEligible` 这样的方法名记录了意图,因此读者无需在大脑中模拟方法体就能理解高级流程。 请注意,四个方法的总圈复杂度之和实际上略高于原始的 8。这没问题,甚至是预期的。对可维护性重要的是**每个方法**的复杂度,因为这是开发人员一次需要推理的单元。 ## 圈复杂度阈值:多高的分数算过高? 没有单一的绝对数字,但文献集中在相同的范围内。下表总结了大多数团队和工具用作指南的内容: | 圈复杂度 | 风险概况 | 实际解释 | | :------- | :------------- | :----------------------------------------------------------------------- | | 1 – 10 | 简单,低风险 | McCabe 的原始建议。易于测试,易于阅读。 | | 11 – 20 | 中等复杂 | 仍可管理,但在代码审查时值得多看一眼。 | | 21 – 50 | 复杂,高风险 | 难以彻底测试。强烈建议重构。 | | > 50 | 不可测试 | Bug 磁铁。通常是需要分解的遗留热点。 | 有两个参考点值得记住。McCabe 本人建议拆分圈复杂度超过 10 的模块。Microsoft 的 CA1502 分析器 (https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1502) 默认将“过度复杂”定义为分数大于 25。Mark Seemann 主张更严格的上限,大约为 7,反映了 Miller 关于人类短期记忆的“神奇数字 7,加减 2”理论。在实践中,正确的阈值取决于代码库。解析器、序列化器或状态机通常能在 15-25 的范围内运行而不被认为客观上差。而业务逻辑得分达到 25 几乎总是有问题的。 ## 在 C# 中度量圈复杂度 无法度量的东西就无法改进,因此使用工具评估代码复杂度至关重要。计算此指标有助于开发人员识别可能需要重构以提高代码质量的领域。NDepend (https://www.ndepend.com/download) 是一个很好的选择,因为它度量 C# 代码中方法的圈复杂度。例如,它包含“按复杂度搜索方法”(https://www.ndepend.com/docs/code-search#Complexity) 功能,有助于识别需要进一步分析的复杂方法。 [](https://blog.ndepend.com/wp-content/uploads/NDepend-C-Cyclomatic-Complexity.png) Visual Studio 本身也提供了“计算代码度量值”命令(*分析 > 计算代码度量值*),它报告每个方法、类型和程序集的圈复杂度。CA1502 分析器可以集成到你的构建中,对超过配置阈值的方法*失败*,这对新代码很有用。基于 Roslyn 的分析器(如 SonarAnalyzer.CSharp)以及第三方工具(如 ReSharper 或 CodeRush)也在编辑器内显示相同的指标。 ## 规范 C# 圈复杂度 NDepend 提供了多个规则,例如“避免过大、过复杂的方法”(https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1003#!),用于标记圈复杂度分数过高的方法,突出显示代码中的潜在问题。 你可能正在处理一个大型遗留代码库,因此重构每个复杂方法是不切实际的。这就是为什么必须将圈复杂度与基线进行比较至关重要,使你能够专注于新增或重构的过于复杂的方法。为此有两个规则: - [从现在起,添加的所有方法都应遵守基本质量原则](https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1102#!) - [避免让复杂方法变得更复杂](https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1104#!) 这种基于基线的方法比任何绝对阈值都重要。当你开始在成熟的代码库上跟踪复杂度时,你不可避免地会发现一些多年来一直稳定且经过良好测试的复杂方法。真正的风险不是静态分数,而是当这些方法*开始增长*时会发生什么。 使用 NDepend 的 CQLinq (https://www.ndepend.com/docs/cqlinq-features),你可以直接表达这个想法: ```cqlinq // 圈复杂度变差 warnif count > 0 from m in JustMyCode.Methods where m.CodeWasChanged() && m.OlderVersion().CyclomaticComplexity <= 10 && m.CyclomaticComplexity > 10 select new { m, OldComplexity = m.OlderVersion().CyclomaticComplexity, m.CyclomaticComplexity } ``` 该查询会在一个已经复杂的方法(CC > 10)在两个分析快照之间变得*更加*复杂时发出警告。换句话说,它突出了**实际有风险**的修改,同时让稳定的遗留代码保持不变。 ## 可视化 C# 圈复杂度 可以使用彩色树状图来可视化你的 C# 方法的圈复杂度 (https://www.ndepend.com/docs/treemap-visualization-of-code-metrics)。在此可视化中,每个矩形代表一个方法: - 矩形的大小对应于方法中的语句数量。 - 矩形的颜色反映方法的圈复杂度。 NDepend 可视化 C# 圈复杂度

相似文章

重新思考LLM集成应用的复杂度度量:超越源代码

arXiv cs.AI

本文介绍了Hecate,这是首个能够量化LLM集成应用中提示层和代码层复杂度的工具。它采用基于霍尔逻辑的Prompt-as-Specification形式化方法,并在开源仓库上评估了52个候选度量,以识别那些能够捕获超出传统纯代码度量的结构广度。

旋转算法再探:clang的libcxx中的循环分解

The Old New Thing (Raymond Chen)

本文深入探讨了clang的libcxx中用于旋转操作的循环分解算法,解释了该算法如何通过计算最大公约数(gcd)来确定循环数量,从而实现最少的交换次数。