C# における巡回的複雑度(Cyclomatic Complexity)
Cyclomatic Complexity in C# (blog.ndepend.com)
要約
C# の巡回的複雑度(CC)は、メソッド内の線形に独立した実行パスの数を数えるコードメトリックです。これは、メソッド内の条件分岐構造の数に 1 を加えたもので、スコアが高いほどコードの可読性、テスト容易性、変更の安全性が低下します。この記事では、CC の計算方法、実用的な閾値、測定・可視化方法、そしてテストカバレッジとの組み合わせによる活用法を C# の例と共に解説します。
全文翻訳
C# における循環的複雑度:知っておくべきことすべて
2026年5月25日 9分で読める
C# における循環的複雑度(またはCC)は、メソッド内の線形独立な実行パスの数を数えるコードメトリクスです。具体的には、メソッド本体内の分岐構造(if、while、for、case、&&、||、?:、??など)の数に1を加えた値として計算されます。スコアが高いほど、そのメソッドは読みにくく、テストしにくく、安全に変更しにくくなります。スコア1は単一の直線的なパスを意味し、約10はThomas McCabeが推奨した伝統的な上限であり、25を超えるものはMicrosoftのCA1502アナライザーによって過剰とフラグ付けされます。
このガイドでは、C#の例を用いて、循環的複雑度がどのように計算されるか、実際にはどの閾値が重要か、実際の.NETコードベースでそれをどのように測定・可視化するか、そしてテストカバレッジやILレベルの分析と組み合わせることで生の値を超えてどのように活用するかを説明します。
目次
- 循環的複雑度とは何か?
- C# における循環的複雑度の定義
- C# における循環的複雑度の影響の例
- 循環的複雑度の閾値:どのスコアが高すぎるか?
- C# における循環的複雑度の測定
- C# 循環的複雑度の管理
- C# 循環的複雑度の可視化
- C# 循環的複雑度とテスト
- 循環的複雑度を超える
- C# における循環的複雑度を減らす方法
- よくある質問
- 結論
循環的複雑度は、1976年にThomas J. McCabeによって、コードの構造的複雑さを定量化する方法として導入されました。この考え方はグラフ理論に由来します。各メソッドは、ノードがステートメントのブロックであり、エッジがそれらの間のジャンプを表す制御フローグラフとして表現できます。そのグラフでは、循環的複雑度は古典的な公式で与えられます。
| 1 | M = E - N + 2P |
ここで、Eはエッジの数、Nはノードの数、Pは連結成分の数です。単一のエントリと単一のエグジットを持つ通常のメソッドの場合、これは1 + 決定ポイントの数に集約されます。これは、ほとんどのツールが実際に計算する形式です。
この数値が実際に示しているのは、メソッド内のすべての独立したパスを実行するために必要なテストケースの最小数です。だからこそ、循環的複雑度はほぼ半世紀も存続しているのです。それは構造的なメトリクスですが、コードの保守やテストを行うすべての人にとって非常に具体的な運用上の意味を持っています。
C# における循環的複雑度の定義
C# メソッドの循環的複雑度は、具体的には以下の式の数に1を加えたものです。
| 1 2 3 4 5 | if while for foreach case default continue goto && || catch 三項演算子 ?: ?? and or |
以下の式はCC計算にはカウントされません。
| 1 2 3 4 5 | else do switch try using throw finally return オブジェクト生成 メソッド呼び出し フィールドアクセス |
人々がつまずく2つの詳細があります。elseは、その対応するifによってすでに代替パスが作成されているため、スコアを増加させません。また、switchは、switchキーワード自体ではなく、case(およびdefault)ごとに1単位を寄与します。C#のパターンマッチング構造(and、or、パターンを持つ最新のスイッチ式)も、従来の対応物と同様にスコアに加算されます。
C# における循環的複雑度の影響の例
複雑なメソッドの提示
ここでは、ifとelseのスコープが絡み合った複雑なメソッドを示します。ifキーワードが6回、&&が1回使用されています。したがって、その循環的複雑度のスコアは8です。
C
| 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 | 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."); } } } |
8つの独立したパスということは、この単一メソッドを完全にカバーするために少なくとも8つのテストが必要であり、新しいビジネスルールを追加するたびにかなりの頭痛の種になります。これは、リグレッションが unnoticed に入り込むまさにそのようなメソッドです。
複雑なメソッドを複数の単純なメソッドにリファクタリングする
上記のメソッドは、より単純な複数のメソッドにリファクタリングできます。コードでは、各メソッドの循環的複雑度スコアをCCと呼びます。
C
| 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 43 44 45 46 47 48 | 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のようなメソッド名は、意図を文書化するため、読者は高レベルのフローを理解するためにメソッド本体を頭の中でシミュレートする必要がありません。
注意点として、4つのメソッド全体で合計された循環的複雑度は、元の8よりもわずかに高くなる場合があります。これは問題なく、むしろ予想されることです。保守性にとって重要なのは、開発者が一度に推論しなければならない単位であるメソッドごとの複雑さです。
循環的複雑度の閾値:どのスコアが高すぎるか?
絶対的な基準はありませんが、文献は同じ範囲に収束しています。以下の表は、ほとんどのチームやツールがガイドラインとして使用しているものをまとめたものです。
| 循環的複雑度 | リスクプロファイル | 実践的な解釈 |
|---|---|---|
| 1 – 10 | 単純、低リスク | McCabeの元の推奨。テストしやすく、読みやすい。 |
| 11 – 20 | 中程度の複雑さ | まだ管理可能ですが、レビュー中に注意が必要です。 |
| 21 – 50 | 複雑、高リスク | 包括的なテストが困難。強力なリファクタリング候補。 |
| > 50 | テスト不能 | バグの温床。分解が必要なレガシーホットスポットであることが多い。 |
2つの参照点は覚えておく価値があります。McCabe自身は、循環的複雑度が10を超えるモジュールを分割することを推奨しました。MicrosoftのCA1502アナライザーは、デフォルトで25を超えるスコアを「過剰な複雑さ」と定義しています。Mark Seemannは、人間の短期記憶のためのミラーの「マジカルナンバー7、プラスマイナス2」を反映して、さらに厳しい上限として約7を主張しています。
実際には、適切な閾値はコードベースに依存します。パーサー、シリアライザー、またはステートマシンは、客観的に悪くない場合でも、通常15〜25の範囲で動作します。25のスコアを持つビジネスロジックは、ほぼ常に問題があります。
C# における循環的複雑度の測定
測定できないものは改善できません。そのため、コードの複雑さを評価するためのツールを使用することが不可欠です。このメトリクスを計算することは、コード品質を向上させるためにリファクタリングが必要な可能性のある領域を開発者が特定するのに役立ちます。
NDependは、C#コードのメソッドの循環的複雑度を測定するため、優れた選択肢です。例えば、複雑度によるメソッドの検索機能が含まれており、さらなる分析のために複雑なメソッドを特定するのに役立ちます。
Visual Studio自体には「コードメトリクスの計算」コマンド(Analyze > Calculate Code Metrics)があり、メソッド、型、アセンブリごとに循環的複雑度を報告します。CA1502アナライザーはビルドに組み込むことができ、設定された閾値を超えるメソッドで実際に失敗させることができます。これは新しいコードに役立ちます。SonarAnalyzer.CSharpのようなRoslynベースのアナライザーや、ReSharperやCodeRushのようなサードパーティツールも、エディタ内で同じメトリクスを表示します。
C# 循環的複雑度の管理
NDependは、大きすぎる、複雑すぎるメソッドを避けるのような複数のルールを提供しており、過度に高い循環的複雑度スコアを持つメソッドにフラグを立て、コード内の潜在的な問題を強調します。
おそらく、あなたは大規模なレガシーコードベースで作業しており、すべての複雑なメソッドをリファクタリングするのは非現実的です。だからこそ、ベースラインに対して循環的複雑度を測定することが不可欠であり、それによって新しい、またはリファクタリングされた複雑すぎるメソッドに集中できるようになります。これには2つのルールがあります。
このベースライン駆動型のアプローチは、絶対的な閾値よりも重要です。確立されたコードベースで複雑さを追跡し始めると、長年安定していてよくテストされてきた複雑なメソッドに必然的に遭遇するでしょう。本当のリスクは静的なスコアではなく、それらのメソッドが成長し始めたときに何が起こるかです。NDependのCQLinqを使用すると、そのアイデアを直接表現できます。
C
| 1 2 3 4 5 6 7 8 | // <Name>循環的複雑度が悪化した</Name> warnif count > 0 from m in JustMyCode.Methods where m.CodeWasChanged() && m.OlderVersion().CyclomaticComplexity < m.CyclomaticComplexity && m.OlderVersion().CyclomaticComplexity > 10 select new { m, OldComplexity = m.OlderVersion().CyclomaticComplexity, m.CyclomaticComplexity } |
このクエリは、すでに複雑なメソッド(CC > 10)が2つの分析スナップショット間でさらに複雑になった場合に警告を発します。つまり、安定したレガシーコードはそのままにして、実際にリスクのある変更を表面化させます。
C# 循環的複雑度の可視化
色付きのトレマップを使用して、C#メソッドの循環的複雑度を可視化できます。この可視化では、各長方形がメソッドを表します。
- 長方形のサイズは、メソッド内のステートメントの数に対応します。
- 長方形の色は、メソッドの循環的複雑度を反映します。
フラットリストに対するトレマップの利点は、複雑さのホットスポットが文字通り画像から飛び出してくることです。穏やかな領域内にある大きくて濃い赤色の長方形は、まさにアーキテクチャの議論に値するメソッドです。
C# 循環的複雑度とテスト
コードのテストを記述することは、今日、すべてのプロフェッショナルなC#開発者にとって不可欠な実践です。通常、テストは単一の実行パスをカバーしますが、メソッドの循環的複雑度スコアは、メソッドを完全にテストするために必要な独立した実行パスの数を表します。したがって、循環的複雑度は、メソッドを完全にテストするために必要なテスト数の大まかな推定値を提供します。
テストを実行することで、各メソッドのコードカバレッジを決定できます。部分的にカバーされたメソッドは、そのすべての独立した実行パスがテストによって挑戦されていないことを意味します。メソッドは低いC.R.A.Pスコアを持つべきというルールは、高い循環的複雑度スコアを持ち、かつテストが不十分なメソッドを検出します(C.R.A.PはChange Risk Analyzer and Predictorの略です)。一致したメソッドは、コードのペインポイントを明確に示しており、テストおよびリファクタリングされるべきです。
[画像: NDepend C# 循環的複雑度とコードカバレッジ]
CRAPスコアは、複雑さとカバレッジを組み合わせるための特定の数学的公式を定義しています。CQLinqで表現すると、その公式は次のようになります。
C
| 1 2 3 4 5 6 7 8 | // <Name>CRAP</Name> from m in JustMyCode.Methods let CC = m.CyclomaticComplexity let uncov = (100 - m.PercentageCoverage) / 100f let CRAP = (CC * CC * uncov * uncov * uncov) + CC select new { m, CRAP } |
CRAPスコアは、複雑さの二乗と未カバー率の三乗でスケールし、その後、生の複雑さをフロアとして加算します。実践的な結論:CC = 30でカバレッジ100%のメソッドは、CC = 12でカバレッジ0%のメソッドよりもはるかにリスクが低いです。カバレッジの2番目のデータポイントは、すべての複雑さが同等ではないことを示しています。
循環的複雑度を超える
循環的複雑度は、コードの複雑さについて推論するための開始点であり、会話の終わりではありません。知っておくべき2つの拡張があります。
複雑度を分岐カバレッジと組み合わせる
循環的複雑度が同じ2つのメソッドがあり、一方は分岐テストで完全にカバーされ、もう一方は全くカバーされていない場合、リスクプロファイルは大きく異なります。この考え方を捉えたCQLinqクエリです。
C
| 1 2 3 4 5 | // <Name>複雑なメソッドの未カバーコード</Name> warnif count > 0 from m in JustMyCode.Methods where m.CyclomaticComplexity > 10 && m.PercentageBranchCoverage < 100 select new { m, m.PercentageBranchCoverage, m.CyclomaticComplexity } |
これは、全分岐カバレッジのない複雑なメソッドに対して警告を発し、「このメソッドはCC > 10である」という単純なリスクビューよりもはるかにニュアンスのあるリスクビューを提供します。
サードパーティコードのためのIL循環的複雑度
非自明なC#プロジェクトは、サードパーティライブラリを取り込みます。ほとんどの場合、それらをブラックボックスとして扱いますが、後で後悔することもあります。NDependのCQLinqは、IL循環的複雑度というプロパティを提供しており、依存しているDLLの内部にある.NET中間言語(IL)に同じメトリクスを適用します。
これにより、候補ライブラリのメソッドの公開APIがどれほどきれいに文書化されているかだけでなく、それらのメソッドが実際にどれほど複雑であるかを測定できます。依存関係を分析して、その内部メソッドがILで定期的にCC = 30を超えていることがわかった場合、問題が発生したときにデバッグが困難になるという強い兆候となります。
C# における循環的複雑度を減らす方法
循環的複雑度を低減するためのリファクタリングは、主にホットパスから決定を抽出することです。以下のテクニックは、実際のコードベースで繰り返し登場します。
1. メソッドの抽出
最も退屈で、最も効果的です。決定の連続したブロックを、前のProcessOrderの例のように、適切に名前が付けられたメソッドに抽出します。各呼び出しサイトは複雑さのチャンクを失い、各新しいメソッドは単独でテストできるほど小さくなります。
2. 早期リターン / ガード句
検証をifブロックに深くネストする代わりに、無効な入力で早期にリターンします。循環的カウントは同じですが、後続のすべての行が入力が有効であることを前提とできるため、認知負荷が大幅に低下します。
C
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // ネストされた public decimal CalculateFee(Order order) { if (order != null) { if (order.IsValid) { if (order.Customer != null) { return order.Total * 0.05m; } } } return 0; } // ガード句で処理 public decimal CalculateFee(Order order) { if (order is null) return 0; if (!order.IsValid) return 0; if (order.Customer is null) return 0; return order.Total * 0.05m; } |
3. 条件分岐をポリモーフィズムに置き換える
型ディスクリミネーターに対する長いswitchは、古典的なコードの臭いです。各caseはスコアに1を加算し、新しいバリアントが登場するたびに変更が必要になるのはそのメソッドだけになります。バリアントごとの動作を派生クラス(Strategy、Template Method、または単純なサブクラスのオーバーライド)に移動すると、通常、中央のメソッドはCC = 1に縮小されます。
4. 最新のC#パターンマッチングとスイッチ式を使用する
スイッチ式はアナライザーによってまだカウントされますが、ネストされたifの連鎖よりもフラットで副作用のないコードを奨励します。パターンマッチングと組み合わせると、CC 8〜10を単一の宣言的な式に置き換えることがよくあります。
C
| 1 2 3 4 5 6 7 | public static decimal DiscountFor(Customer c) => c switch { { IsVip: true, YearsActive: >= 5 } => 0.20m, { IsVip: true } => 0.10m, { YearsActive: >= 10 } => 0.08m, { YearsActive: >= 1 } => 0.03m, _ => 0m }; |
5. ルックアップテーブルを純粋なマッピングに使用する
メソッドが主に「入力Xは出力Yにマッピングされる」というリストである場合、Dictionary<TKey, TValue>または静的な読み取り専用配列は、ifの連鎖よりもほぼ常に優れています。辞書ルックアップは、エントリの数に関係なくCC = 1です。
6. ブーリアンパラメータはコードの臭い
複数のboolフラグを受け取るメソッドは、ほぼ常にトレンチコートを着た複数のメソッドを隠しています。SendEmail(bool html, bool urgent, bool dryRun)をより具体的なメソッドに分割することで、メソッドごとの複雑さと、呼び出し元がフラグの誤った組み合わせを渡す可能性の両方が減少します。
よくある質問
C# における良い循環的複雑度のスコアは?
10未満は安全と見なされ、10から20は注意が必要で、25を超えるものはMicrosoftのCA1502ルールによって過剰とフラグ付けされます。McCabeの元の推奨事項(依然として広く引用されている)は、10を超えるメソッドをリファクタリングすることです。
else キーワードは循環的複雑度を増加させますか?
いいえ。elseブランチは、対応するifによってすでに暗示されているため、新しい独立したパスを追加しません。if自体のみがカウントされます。
switch 文は1回またはケースごとにカウントされますか?
caseごと(およびdefaultごと)です。switchキーワード自体はスコアを増加させません。6つのcaseと1つのdefaultを持つスイッチは、メソッドの循環的複雑度に7を加算します。
循環的複雑度は必要な単体テストの数と等しいですか?
有用な下限であり、契約ではありません。循環的複雑度は、線形独立なパスの数を提供します。これは、完全なパスカバレッジに必要なテストの最小数です。実際には、それより少ない(一部のパスは実行不可能である)か、または多い(単一のパス内のデータ組み合わせが依然として誤動作する可能性がある)場合があります。
循環的複雑度と認知複雑度の違いは何ですか?
循環的複雑度はパスの数を測定します。認知複雑度(SonarSourceによって普及)は、コードを読みやすくするネストの深さとブーリアン演算子の組み合わせも考慮に入れます。これら2つのメトリクスは補完的です。循環的複雑度が低く、認知複雑度が高いのはまれです。認知複雑度が低く、循環的複雑度が高いのもまれです。一方のメトリクスで悪いメソッドは、通常もう一方でも悪いです。
Visual Studio で循環的複雑度を測定するにはどうすればよいですか?
Visual Studioでは、Analyze > Calculate Code Metrics > For Solutionに移動します。表示されるウィンドウには、メソッド、型、プロジェクトごとの循環的複雑度がリストされます。継続的な強制のために、CA1502アナライザーを有効にし、CodeMetricsConfig.txt追加ファイルを通じてその閾値を設定します。
循環的複雑度は async または LINQ コードで機能しますか?
はい。C#コンパイラはasync/awaitをステートマシンに書き換えますが、ほとんどのツール(NDepend、Roslynアナライザー、Visual Studioのメトリクス)はソースレベルのメソッドに対して循環的複雑度を計算します。await自体はカウントされませんが、それを囲むif、while、catch構造はカウントされます。LINQクエリ演算子はメソッド呼び出しであり、それらもカウントされませんが、それらに渡されるラムダは独自のメソッドとして分析されます。
結論
循環的複雑度は、現在も活発に使用されている最も古いコードメトリクスの1つであり、その理由は単純です。それは、現実世界の苦痛に直接対応するものを捉えています。高いスコアは、推論すべきパスが多く、書くべきテストが多く、バグが潜む場所が多いことを意味します。特に最も頻繁に変更されるメソッドの複雑さを管理下に置くことは、数週間で元が取れます。
とはいえ、スコアだけでは全体像の半分にすぎません。複雑だが十分にテストされたメソッドは、夜中にあなたを起こすようなものではありません。カバレッジがゼロで、スプリントごとに触られる中程度の複雑さのメソッドがそうです。循環的複雑度をカバレッジ(またはCRAPスコア)と組み合わせ、レガシーコードではフラットな閾値ではなくデルタベースのルールを強制し、IL循環的複雑度を使用して依存しているライブラリを健全性チェックしてください。これらを組み合わせることで、1970年代のメトリクスは、技術的負債に対する驚くほど現代的な早期警告システムに変わります。
自分のコードベースでこれを試したい場合は、NDependの無料トライアルをダウンロードして分析を実行してください。複雑さのホットスポットは、通常、最初の数分で明らかになります。
この記事は、コードの保守性、セキュリティ、および全体的な品質の向上を支援する実績のある.NET静的分析ツールであるNDependのチームがお届けしました。レガシー.NETアプリケーションを最新化する場合でも、C#でゼロから始める場合でも、今日から無料のフル機能トライアルを始めましょう!