C#の開発において、キーと値のペアを管理するDictionary<TKey, TValue>は、最も利用頻度の高いコレクションの一つです。

データの検索効率に優れる一方で、保持している全要素に対して処理を行いたい場面も多く、その際に中心となるのがforeach文によるループ処理です。

現代のC#(特にC# 7.0以降)では、タプル形式の分解(Deconstruction)を用いることで、かつてよりも簡潔かつ直感的に辞書データを列挙できるようになりました。

本記事では、基本的なループ方法から、最新の構文を用いた効率的な記述、さらにはパフォーマンスや実行時の注意点までをテクニカルな視点で詳しく解説します。

C#におけるDictionaryの基本構造と列挙

Dictionary<TKey, TValue>は、内部的にはハッシュテーブルを利用したデータ構造です。

このコレクションをforeachでループする際、列挙される要素はデフォルトでKeyValuePair<TKey, TValue>という構造体になります。

まずは、最も伝統的かつ基本となる列挙方法を確認しましょう。

KeyValuePairを使用した伝統的な列挙

C#の初期から利用されているこの方法では、ループ変数としてKeyValuePairを受け取ります。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 辞書の初期化
        var userScores = new Dictionary<string, int>
        {
            { "Alice", 85 },
            { "Bob", 92 },
            { "Charlie", 78 }
        };

        // KeyValuePairを使用したループ
        foreach (KeyValuePair<string, int> entry in userScores)
        {
            // KeyとValueプロパティからそれぞれの値にアクセス
            Console.WriteLine($"Name: {entry.Key}, Score: {entry.Value}");
        }
    }
}
実行結果
Name: Alice, Score: 85
Name: Bob, Score: 92
Name: Charlie, Score: 78

この方法のメリットは、型が明示的であるため、古いコードベースとの互換性が高く、初心者にとっても構造が理解しやすい点にあります。

しかし、entry.Keyentry.Valueといったプロパティを都度参照する必要があり、コードが冗長になりやすいという側面もあります。

C# 7.0以降の推奨:分解(Deconstruction)によるループ

現代的なC#開発において、最も推奨されるのが分解(Deconstruction)を利用した記述です。

C# 7.0で導入されたこの機能により、KeyValuePair構造体を意識することなく、直接「キー」と「値」を個別の変数として取得できます。

分解を利用したコード例

構文は非常にシンプルで、foreachの変数部分をタプルのように(key, value)と記述するだけです。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var inventory = new Dictionary<string, int>
        {
            { "Apple", 50 },
            { "Orange", 30 },
            { "Banana", 20 }
        };

        // 分解(Deconstruction)を利用したスマートなループ
        foreach (var (product, count) in inventory)
        {
            // 直接変数として利用可能
            Console.WriteLine($"{product}の在庫は残り{count}個です。");
        }
    }
}
実行結果
Appleの在庫は残り50個です。
Orangeの在庫は残り30個です。
Bananaの在庫は残り20個です。

この書き方の利点は、コードの可読性が劇的に向上することです。

entry.Keyのような間接的なアクセスが不要になり、変数名(上記の例ではproductcount)にドメイン固有の意味を持たせやすくなります。

分解が動作する仕組み

なぜDictionaryに対してこの構文が使えるのかというと、.NETのKeyValuePair<TKey, TValue>構造体にDeconstructというメソッドが定義されているからです。

コンパイラはこのメソッドを呼び出し、構造体の中身を複数の変数へ展開します。

特定の要素(KeysまたはValues)のみをループする

場合によっては、辞書全体のペアではなく、キーのみ、あるいは値のみを列挙したいケースがあります。

この場合、辞書のプロパティであるKeysまたはValuesに直接アクセスすることで、処理の意図をより明確にできます。

Keysプロパティの列挙

キーだけが必要な場合は、dict.Keysを使用します。

C#
foreach (var name in userScores.Keys)
{
    Console.WriteLine($"登録ユーザー: {name}");
}

Valuesプロパティの列挙

値だけが必要な場合は、dict.Valuesを使用します。

平均値の算出や合計値の計算などに便利です。

C#
int totalScore = 0;
foreach (var score in userScores.Values)
{
    totalScore += score;
}

パフォーマンス上の注意

よくある誤用として、Keysをループしながら、ループ内でdict[key]を使って値を取得するパターンがあります。

C#
// 非推奨な書き方
foreach (var key in dict.Keys)
{
    var value = dict[key]; // 無駄なハッシュ計算が再度発生する
    // 処理
}

この書き方は、内部で再度キーの検索処理(ハッシュ計算と探索)が走るため、効率が良くありません

キーと値の両方が必要な場合は、必ず前述したforeach (var (key, value) in dict)の形式を使用するようにしましょう。

ループ処理中の注意点と禁じ手

Dictionaryのループ処理において、最も注意しなければならないのが「反復中のコレクションの変更」です。

実行時エラー:InvalidOperationException

foreachでDictionaryを回している最中に、そのDictionaryに対して要素を追加したり、削除したりすることは禁止されています。

これを実行すると、実行時にInvalidOperationExceptionがスローされます。

C#
var dict = new Dictionary<string, int> { { "A", 1 }, { "B", 2 } };

try
{
    foreach (var key in dict.Keys)
    {
        if (key == "A")
        {
            dict.Remove(key); // ここで例外が発生
        }
    }
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"エラー: {ex.Message}");
}

回避策:削除対象をリスト化する

ループ中に要素を削除したい場合は、一度削除したいキーをListなどに退避させ、ループが終わった後にまとめて削除するのが定石です。

C#
var keysToRemove = new List<string>();

// まず削除対象を特定する
foreach (var (key, value) in dict)
{
    if (value < 10)
    {
        keysToRemove.Add(key);
    }
}

// ループの外で削除
foreach (var key in keysToRemove)
{
    dict.Remove(key);
}

また、LINQのToList()を利用して、コレクションのコピーに対してループを回すという手法もありますが、メモリ効率を考慮すると、削除対象のキーのみを保持するほうが軽量です。

パフォーマンスと最適化のヒント

大規模なデータを扱う場合、ループの書き方ひとつでアプリケーションのパフォーマンスに影響が出ることがあります。

列挙のコストと構造体

Dictionaryの列挙子は構造体(Enumerator)として実装されており、ヒープメモリへの割り当て(アロケーション)を最小限に抑える工夫がなされています。

そのため、基本的には標準のforeachを使用していれば、大きなオーバーヘッドを心配する必要はありません。

LINQのSelectやWhereとの使い分け

LINQのforeach(厳密にはList<T>.ForEachや、LINQメソッドの組み合わせ)を使用することも可能ですが、単純な列挙であればforeach文のほうが高速であり、デバッグもしやすいという特徴があります。

手法可読性パフォーマンス主な用途
foreach (var (k, v) in dict)非常に高い高い基本的な全件処理
foreach (var k in dict.Keys)高い高いキーのみの利用
LINQ (.Where().Select())非常に高い中程度複雑なフィルタリングと変換
Parallel.ForEach中程度高(並列)重い計算処理を含む大量データ

読み取り専用としての列挙

マルチスレッド環境においてDictionaryを列挙する場合、他のスレッドから変更が入ると前述の例外が発生します。

スレッドセーフな列挙が必要な場合は、ConcurrentDictionaryの使用を検討するか、列挙前にlockを取得するなどの対策が必要です。

高度なトピック:Span<T>や最新ランタイムでの最適化

.NET 8以降などの最新環境では、辞書の内部構造に直接アクセスするような高度な最適化も進んでいますが、一般的なビジネスロジックにおいてはforeachの分解構文で十分な速度が得られます。

もし、数百万件の辞書を極限まで高速に走査したい場合は、CollectionsMarshal.AsSpanなどの特殊なAPI(主にリスト向けですが、辞書でも内部バッファの扱いに言及されることがあります)や、構造体のコピーを避けるための参照渡し(ref)の議論がありますが、これらは可読性を著しく損なうため、ボトルネックが特定された場合のみ適用すべきです。

まとめ

C#においてDictionaryをforeachでループ処理する方法は、言語の進化とともに洗練されてきました。

  • C# 7.0以降であれば、分解構文 foreach (var (key, value) in dict) を使用するのが最も美しく、効率的です。
  • キーのみ、または値のみが必要な場合は、dict.Keysdict.Values を適切に使い分けましょう。
  • ループ内での要素の追加・削除は「厳禁」であり、必ずリストなどを用いてループ外で操作するようにします。
  • 基本的なforeachは十分に最適化されており、無理に複雑な最適化を行う必要はありません。

これらのルールを意識することで、バグが少なく、かつ意図が明確に伝わるC#コードを記述できるようになります。

日々の開発では、まず「可読性の高い分解構文」を選択することを心がけてみてください。