C#の連想配列はDictionaryで解決!宣言・追加・検索・削除まで初心者向けに徹底解説

はじめに

C#で「文字列の名前から値を取得したい」「商品コードと商品情報を対応付けたい」と考えたときに役立つのが、Dictionary<TKey, TValue>です。

C#には「連想配列」という名前の型はありませんが、キーと値をセットで管理するDictionary<TKey, TValue>を使えば、一般的な連想配列と同じ仕組みを実現できます。

C#
var users = new Dictionary<int, string>
{
{ 1, "田中" },
{ 2, "鈴木" }
};

Console.WriteLine(users[1]); // 田中

この記事では、C#の連想配列に相当するDictionaryについて、宣言・初期化・追加・検索・更新・削除・繰り返し処理まで、初心者にもわかりやすく解説します。

1. C#の連想配列はDictionaryを使えば実現できる

1-1. 連想配列とはキーと値をセットで管理するデータ構造

連想配列とは、データを「キー」と「値」の組み合わせで管理するデータ構造です。

たとえば、商品コードと商品名を管理する場合、次のような対応関係になります。

キー
P001ノートパソコン
P002マウス
P003キーボード

通常の配列では、01などのインデックスを指定して要素を取得します。一方、連想配列では"P001"のような意味のあるキーを指定して値を取得できます。

C#
Console.WriteLine(products["P001"]);

キーから値を直接検索できるため、データの意味がわかりやすく、必要な情報を効率的に取得できます。

1-2. C#ではDictionary<TKey, TValue>が連想配列に相当する

C#で連想配列を実現するときは、System.Collections.Generic名前空間のDictionary<TKey, TValue>を使用します。

TKeyはキーの型、TValueは値の型を表します。

C#
Dictionary<string, int> scores = new Dictionary<string, int>();

この例では、キーがstring型、値がint型です。

C#
scores.Add("田中", 90);
scores.Add("鈴木", 85);

Console.WriteLine(scores["田中"]); // 90

Dictionaryはハッシュテーブルを利用して実装されており、キーによる値の取得は通常、O(1)に近い高速な処理です。キーの重複は許可されませんが、値は重複しても問題ありません。Microsoft Learn

1-3. 配列・List・Dictionaryの違い

配列、List<T>Dictionary<TKey, TValue>には、次のような違いがあります。

種類要素の指定方法サイズ変更主な用途
配列数値インデックス不可要素数が固定されたデータ
List<T>数値インデックス順番に並ぶ可変長データ
Dictionary<TKey, TValue>任意のキーキーと値の対応関係

配列とListでは、通常は位置を指定して値を取得します。

C#
var names = new List<string> { "田中", "鈴木" };

Console.WriteLine(names[0]); // 田中

Dictionaryでは、キーを指定して値を取得します。

C#
var users = new Dictionary<int, string>
{
{ 1001, "田中" },
{ 1002, "鈴木" }
};

Console.WriteLine(users[1001]); // 田中

順番が重要なデータには配列やList、識別子から値を検索するデータにはDictionaryが適しています。

1-4. Dictionaryが適している場面と使用例

Dictionaryは、次のような場面に適しています。

  • 商品コードと商品情報を対応付ける

  • ユーザーIDからユーザー情報を検索する

  • 都道府県コードと都道府県名を管理する

  • 設定名と設定値を管理する

  • 単語ごとの出現回数を数える

  • エラーコードとメッセージを対応付ける

たとえば、HTTPステータスコードと説明を管理できます。

C#
var statusMessages = new Dictionary<int, string>
{
{ 200, "OK" },
{ 404, "Not Found" },
{ 500, "Internal Server Error" }
};

Console.WriteLine(statusMessages[404]);

キーを使ってデータを検索したい場合は、まずDictionaryを検討するとよいでしょう。

2. Dictionaryを宣言・初期化する方法

2-1. Dictionaryを使うためのusingディレクティブ

Dictionary<TKey, TValue>System.Collections.Generic名前空間に含まれています。

ファイルの先頭に次のusingディレクティブを記述します。

C#
using System.Collections.Generic;

最近の.NETプロジェクトでは暗黙的なusingが有効になっており、明示的に書かなくても利用できる場合があります。

ただし、Dictionaryが見つからないというコンパイルエラーが出た場合は、using System.Collections.Generic;が記述されているか確認してください。

2-2. Dictionary<TKey, TValue>の基本的な宣言方法

基本的な宣言方法は次のとおりです。

C#
Dictionary<string, int> scores = new Dictionary<string, int>();

山かっこ内の最初の型がキー、2番目の型が値です。

C#
Dictionary<int, string> users = new Dictionary<int, string>();
Dictionary<string, decimal> prices = new Dictionary<string, decimal>();
Dictionary<Guid, DateTime> loginDates = new Dictionary<Guid, DateTime>();

宣言時点では要素が1件も登録されていないため、Count0です。

C#
Console.WriteLine(scores.Count); // 0

2-3. コレクション初期化子を使って要素を設定する方法

宣言と同時に要素を登録する場合は、コレクション初期化子を使用できます。

C#
var scores = new Dictionary<string, int>
{
{ "田中", 90 },
{ "鈴木", 85 },
{ "佐藤", 78 }
};

インデクサー形式の初期化も可能です。

C#
var scores = new Dictionary<string, int>
{
["田中"] = 90,
["鈴木"] = 85,
["佐藤"] = 78
};

{ キー, 値 }形式では同じキーが複数あると初期化時に例外が発生します。インデクサー形式では、同じキーが後から指定された場合に値が上書きされます。

2-4. varを使ってDictionaryを宣言する方法

右辺から型を推論できる場合は、varを使って宣言できます。

C#
var scores = new Dictionary<string, int>();

変数の型そのものが動的になるわけではありません。コンパイル時にはDictionary<string, int>型として確定します。

初期化子を使う場合も同様です。

C#
var products = new Dictionary<string, decimal>
{
{ "ノート", 150m },
{ "ペン", 100m }
};

型名の重複を減らせるため、右辺を見れば型が明確な場合はvarがよく使われます。

2-5. キーや値に独自クラスを指定する方法

値には独自クラスも指定できます。

C#
public class User
{
public int Id { get; init; }
public string Name { get; init; } = "";
public string Email { get; init; } = "";
}
C#
var users = new Dictionary<int, User>
{
{
1001,
new User
{
Id = 1001,
Name = "田中",
Email = "tanaka@example.com"
}
}
};

Console.WriteLine(users[1001].Name);

独自クラスをキーにすることも可能です。

C#
public record ProductKey(string Category, int Number);

var products = new Dictionary<ProductKey, string>
{
{ new ProductKey("PC", 1), "ノートパソコン" }
};

独自クラスをキーにする場合は、キー同士の等価性とハッシュ値が適切に判定される必要があります。recordを使うと、値に基づく等価比較を簡潔に実現できます。

2-6. 大文字・小文字を区別しないDictionaryの作り方

文字列キーでは、通常は大文字と小文字が区別されます。

C#
var settings = new Dictionary<string, string>();

settings["Language"] = "ja";

Console.WriteLine(settings.ContainsKey("language")); // False

大文字と小文字を区別したくない場合は、コンストラクターにStringComparerを渡します。

C#
var settings = new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase
);

settings["Language"] = "ja";

Console.WriteLine(settings["language"]); // ja
Console.WriteLine(settings["LANGUAGE"]); // ja

識別子や設定名など、文化圏に依存しない文字列を比較する場合は、一般にStringComparer.OrdinalIgnoreCaseが適しています。

3. Dictionaryに要素を追加する方法

3-1. Addメソッドでキーと値を追加する

Addメソッドは、キーと値を新しい要素として追加します。

C#
var products = new Dictionary<string, string>();

products.Add("P001", "ノートパソコン");
products.Add("P002", "マウス");

登録済みのキーを誤って追加した場合は、例外によって問題を検出できます。

C#
products.Add("P001", "デスクトップパソコン");
// ArgumentException

重複がプログラム上の不具合であり、必ず検出したい場合に向いています。

3-2. インデクサーを使って要素を追加する

存在しないキーをインデクサーに指定して値を代入すると、新しい要素が追加されます。

C#
var products = new Dictionary<string, string>();

products["P001"] = "ノートパソコン";
products["P002"] = "マウス";

キーがすでに存在する場合は、既存の値が上書きされます。

C#
products["P001"] = "ゲーミングノートパソコン";

追加と更新を同じ書き方で行いたい場合に便利ですが、意図しない上書きには注意が必要です。Addは重複キーで例外を発生させるのに対し、インデクサーは既存値を上書きします。Microsoft Learn

3-3. TryAddメソッドで重複を避けて追加する

TryAddメソッドは、指定したキーが存在しない場合だけ要素を追加します。

C#
var products = new Dictionary<string, string>();

bool added = products.TryAdd("P001", "ノートパソコン");

Console.WriteLine(added); // True

同じキーをもう一度追加すると、例外を発生させずにfalseを返します。

C#
bool addedAgain = products.TryAdd("P001", "マウス");

Console.WriteLine(addedAgain); // False
Console.WriteLine(products["P001"]); // ノートパソコン

追加できたかどうかを戻り値で判断できるため、重複が正常に起こり得る処理に適しています。

3-4. 同じキーを追加すると発生する例外

Addメソッドで同じキーを追加すると、ArgumentExceptionが発生します。

C#
var users = new Dictionary<int, string>();

users.Add(1, "田中");
users.Add(1, "鈴木");

例外処理で対応することもできます。

C#
try
{
users.Add(1, "鈴木");
}
catch (ArgumentException)
{
Console.WriteLine("同じユーザーIDが登録されています。");
}

ただし、重複が想定内である場合に例外を制御フローとして利用するのは避け、TryAddを使用するほうが明確です。

3-5. Add・インデクサー・TryAddの使い分け

3つの追加方法は、次の基準で使い分けます。

方法キーが存在しない場合キーが存在する場合
Add追加例外
インデクサー追加上書き
TryAdd追加してtrue何もせずfalse

重複を不具合として検出したい場合はAdd、追加と更新をまとめて行う場合はインデクサー、重複を安全に判定したい場合はTryAddが適しています。

C#
products.Add("P001", "ノートパソコン");
products["P002"] = "マウス";

if (!products.TryAdd("P001", "キーボード"))
{
Console.WriteLine("P001はすでに登録されています。");
}

4. Dictionaryから値を取得・検索する方法

4-1. キーを指定して値を取得する

インデクサーにキーを指定すると、対応する値を取得できます。

C#
var products = new Dictionary<string, string>
{
{ "P001", "ノートパソコン" },
{ "P002", "マウス" }
};

string productName = products["P001"];

Console.WriteLine(productName);

キーの存在が確実な場合は、簡潔で読みやすい方法です。

ただし、存在しないキーを指定するとKeyNotFoundExceptionが発生します。

4-2. ContainsKeyメソッドでキーの存在を確認する

ContainsKeyメソッドを使うと、キーが登録されているか確認できます。

C#
if (products.ContainsKey("P001"))
{
Console.WriteLine(products["P001"]);
}
else
{
Console.WriteLine("商品が見つかりません。");
}

値を取得せず、キーが存在するかだけを調べたい場合に適しています。

ただし、存在確認後に値を取得すると、検索処理が2回行われます。値も必要な場合はTryGetValueのほうが効率的です。

4-3. TryGetValueメソッドで安全に値を取得する

TryGetValueは、キーの存在確認と値の取得を同時に行います。

C#
if (products.TryGetValue("P001", out string? productName))
{
Console.WriteLine(productName);
}
else
{
Console.WriteLine("商品が見つかりません。");
}

型を省略して次のようにも書けます。

C#
if (products.TryGetValue("P001", out var productName))
{
Console.WriteLine(productName);
}

キーが存在すればtrue、存在しなければfalseを返します。存在しないキーを検索する可能性がある場合、インデクサーの例外を捕捉するよりTryGetValueを利用するほうが効率的です。Microsoft Learn

4-4. ContainsValueメソッドで値の存在を検索する

ContainsValueメソッドでは、指定した値が登録されているか確認できます。

C#
bool exists = products.ContainsValue("マウス");

Console.WriteLine(exists); // True

ただし、Dictionaryはキーから値を検索することを目的としたデータ構造です。ContainsValueは要素を順番に調べるため、要素数をnとするとO(n)の処理になります。Microsoft Learn

値による検索を頻繁に行う場合は、別のDictionaryを用意する、HashSet<T>を併用する、データ構造を見直すといった対応を検討しましょう。

4-5. LINQを使って条件に一致する要素を検索する

値の一部や複雑な条件で検索したい場合は、LINQを利用できます。

C#
using System.Linq;

var prices = new Dictionary<string, decimal>
{
{ "ノートパソコン", 120000m },
{ "マウス", 3000m },
{ "キーボード", 8000m }
};

var expensiveProducts = prices
.Where(x => x.Value >= 5000m)
.ToList();

foreach (var product in expensiveProducts)
{
Console.WriteLine($"{product.Key}: {product.Value}円");
}

キーを条件に検索することもできます。

C#
var result = prices
.Where(x => x.Key.Contains("パソコン"));

LINQによる絞り込みは基本的に全要素を調べます。単一キーを検索できる場合は、TryGetValueを優先しましょう。

4-6. 存在しないキーを指定したときのエラー対策

存在しないキーをインデクサーで取得すると、KeyNotFoundExceptionが発生します。

C#
Console.WriteLine(products["P999"]);

安全に取得するには、主に次の方法があります。

C#
if (products.TryGetValue("P999", out var productName))
{
Console.WriteLine(productName);
}

既定値を返したい場合は、GetValueOrDefaultも利用できます。

C#
string? name = products.GetValueOrDefault("P999");

独自の既定値を指定することも可能です。

C#
string name = products.GetValueOrDefault(
"P999",
"未登録の商品"
);

Console.WriteLine(name);

キーが存在しないことが通常のケースなら、例外処理ではなくTryGetValueGetValueOrDefaultを使用しましょう。

5. Dictionaryの値を更新・変更する方法

5-1. インデクサーを使って既存の値を更新する

既存キーの値を更新するには、インデクサーで新しい値を代入します。

C#
var scores = new Dictionary<string, int>
{
{ "田中", 80 }
};

scores["田中"] = 95;

Console.WriteLine(scores["田中"]); // 95

最も簡単な更新方法ですが、キーが存在しない場合は新しい要素が追加されます。

5-2. キーの存在を確認してから値を更新する

既存の要素だけを更新したい場合は、ContainsKeyで確認します。

C#
if (scores.ContainsKey("田中"))
{
scores["田中"] = 95;
}
else
{
Console.WriteLine("更新対象が存在しません。");
}

キーがない場合に追加したくない処理では、存在確認を行う必要があります。

5-3. TryGetValueを使って現在値を取得して更新する

現在の値を使って更新する場合は、TryGetValueが便利です。

C#
if (scores.TryGetValue("田中", out int currentScore))
{
scores["田中"] = currentScore + 5;
}

単語の出現回数や在庫数など、現在値を増減させる処理でよく使います。

C#
if (stock.TryGetValue("P001", out int currentStock))
{
stock["P001"] = currentStock - 1;
}

5-4. Dictionaryのキーを変更する方法

Dictionaryには、キーだけを直接変更する機能はありません。

キーを変更したい場合は、古いキーの要素を削除して、新しいキーで追加します。

C#
var products = new Dictionary<string, string>
{
{ "P001", "ノートパソコン" }
};

if (products.Remove("P001", out string? productName))
{
products.Add("PC001", productName);
}

新しいキーがすでに存在するとAddで例外が発生するため、必要に応じて事前に確認します。

C#
if (!products.ContainsKey("PC001") &&
products.Remove("P001", out var productName))
{
products.Add("PC001", productName);
}

5-5. 値がコレクションの場合に要素を追加・変更する方法

Dictionaryの値には、List<T>などのコレクションも指定できます。

C#
var categories = new Dictionary<string, List<string>>
{
{
"パソコン",
new List<string> { "ノートパソコン" }
}
};

登録済みのリストに要素を追加する場合は、値として取得したListを操作します。

C#
categories["パソコン"].Add("デスクトップパソコン");

キーが存在しない可能性がある場合は、次のように処理できます。

C#
string category = "周辺機器";
string productName = "マウス";

if (!categories.TryGetValue(category, out var productList))
{
productList = new List<string>();
categories[category] = productList;
}

productList.Add(productName);

6. Dictionaryから要素を削除する方法

6-1. Removeメソッドで指定したキーの要素を削除する

指定したキーの要素を削除するには、Removeメソッドを使います。

C#
var products = new Dictionary<string, string>
{
{ "P001", "ノートパソコン" },
{ "P002", "マウス" }
};

bool removed = products.Remove("P001");

Console.WriteLine(removed); // True

削除に成功するとtrue、キーが存在しなければfalseを返します。

C#
if (!products.Remove("P999"))
{
Console.WriteLine("削除対象が見つかりません。");
}

6-2. 削除対象の値を取得しながらRemoveする方法

Removeには、削除した値をout引数で受け取るオーバーロードがあります。

C#
if (products.Remove("P002", out string? removedProduct))
{
Console.WriteLine($"{removedProduct}を削除しました。");
}

削除前にインデクサーで値を取得する必要がないため、検索と削除をまとめて実行できます。Microsoft Learn

6-3. Clearメソッドですべての要素を削除する

すべての要素を削除するには、Clearメソッドを使います。

C#
products.Clear();

Console.WriteLine(products.Count); // 0

Dictionary変数自体がnullになるわけではなく、空のDictionaryとして引き続き利用できます。

C#
products["P003"] = "キーボード";

6-4. 条件に一致する複数の要素を削除する方法

Dictionaryには、条件に一致する要素をまとめて削除する専用メソッドがありません。

削除対象のキーを一度配列やリストに変換してから削除します。

C#
var prices = new Dictionary<string, decimal>
{
{ "ノートパソコン", 120000m },
{ "マウス", 3000m },
{ "キーボード", 8000m }
};

var keysToRemove = prices
.Where(x => x.Value < 5000m)
.Select(x => x.Key)
.ToArray();

foreach (var key in keysToRemove)
{
prices.Remove(key);
}

先にToArrayToListで削除対象を確定させることが重要です。

6-5. foreach中に要素を削除するときの注意点

通常、Dictionaryforeachで列挙している最中に要素を変更すると、列挙処理が無効になり、例外が発生する可能性があります。

次のようなコードは避けましょう。

C#
foreach (var item in prices)
{
if (item.Value < 5000m)
{
prices.Remove(item.Key);
}
}

安全に削除するには、キーを別のコレクションへコピーします。

C#
foreach (var key in prices
.Where(x => x.Value < 5000m)
.Select(x => x.Key)
.ToList())
{
prices.Remove(key);
}

追加や更新についても、列挙中に元のDictionaryを変更しない設計にすると安全です。

7. Dictionaryの要素を繰り返し処理する方法

7-1. foreachでキーと値を同時に取得する

foreachを使うと、すべてのキーと値を順番に処理できます。

C#
var products = new Dictionary<string, string>
{
{ "P001", "ノートパソコン" },
{ "P002", "マウス" }
};

foreach (var item in products)
{
Console.WriteLine(
$"キー: {item.Key}, 値: {item.Value}"
);
}

分解構文を使うと、キーと値を個別の変数で受け取れます。

C#
foreach (var (code, name) in products)
{
Console.WriteLine($"{code}: {name}");
}

7-2. Keysプロパティを使ってキーだけを列挙する

キーだけが必要な場合は、Keysプロパティを使用します。

C#
foreach (string code in products.Keys)
{
Console.WriteLine(code);
}

Keysは元のDictionaryを参照するコレクションです。取得した時点の独立したコピーではありません。元のDictionaryの変更はKeysにも反映されます。Microsoft Learn

独立したコピーが必要な場合は、配列やリストへ変換します。

C#
string[] codes = products.Keys.ToArray();

7-3. Valuesプロパティを使って値だけを列挙する

値だけを処理する場合は、Valuesプロパティを使います。

C#
foreach (string name in products.Values)
{
Console.WriteLine(name);
}

値には重複が許可されるため、同じ値が複数回列挙される場合があります。

重複を除外したい場合は、LINQのDistinctを使用します。

C#
foreach (string name in products.Values.Distinct())
{
Console.WriteLine(name);
}

7-4. KeyValuePairを使った要素の受け取り方

Dictionaryの各要素は、KeyValuePair<TKey, TValue>として取得されます。

C#
foreach (KeyValuePair<string, string> item in products)
{
string key = item.Key;
string value = item.Value;

Console.WriteLine($"{key}: {value}");
}

メソッドの引数や戻り値でキーと値のペアを扱う場合にも利用できます。

C#
static void ShowProduct(
KeyValuePair<string, string> product)
{
Console.WriteLine(
$"{product.Key}: {product.Value}"
);
}

7-5. LINQで並び替え・絞り込みを行う方法

キー順に処理するには、OrderByを使います。

C#
foreach (var item in products.OrderBy(x => x.Key))
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

値順に並び替える場合は、Valueを指定します。

C#
foreach (var item in products.OrderBy(x => x.Value))
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

条件による絞り込みと組み合わせることも可能です。

C#
var result = products
.Where(x => x.Value.Contains("パソコン"))
.OrderBy(x => x.Key);

foreach (var item in result)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

LINQで並び替えても、元のDictionary自体の並び順が変更されるわけではありません。

8. Dictionaryでよく使うプロパティとメソッド

8-1. Countプロパティで要素数を取得する

Countプロパティでは、登録されているキーと値のペアの数を取得できます。

C#
Console.WriteLine(products.Count);

空かどうかを確認する場合は、次のように記述できます。

C#
if (products.Count == 0)
{
Console.WriteLine("商品が登録されていません。");
}

1件以上あることを調べるなら、Count > 0と書けます。

C#
if (products.Count > 0)
{
Console.WriteLine("商品が登録されています。");
}

8-2. EnsureCapacityで必要な容量を確保する

大量の要素を追加する予定がある場合は、EnsureCapacityで必要な容量を事前に確保できます。

C#
var users = new Dictionary<int, string>();

users.EnsureCapacity(10_000);

Dictionaryの容量は、内部データを再配置せずに保持できる要素数です。EnsureCapacityを使うと、指定件数まで追加するときの容量拡張を抑えられます。Microsoft Learn

最終的な要素数がわかっている場合は、コンストラクターで初期容量を指定する方法もあります。

C#
var users = new Dictionary<int, string>(10_000);

一般的な小規模データでは、容量を手動で調整する必要はありません。

8-3. TrimExcessで余分な容量を削減する

多数の要素を削除した後は、内部容量が要素数より大きい状態になることがあります。

TrimExcessを使うと、余分な容量を削減できます。

C#
users.Clear();
users.TrimExcess();

現在の要素数に応じて容量を調整するため、長期間保持する大きなDictionaryのメモリ使用量を抑えたい場合に有効です。Microsoft Learn

ただし、容量の調整には処理コストがかかります。その後すぐに大量の要素を追加する場合は、再度容量が拡張されるため、頻繁に呼び出すべきではありません。

8-4. ToDictionaryでコレクションをDictionaryに変換する

配列やListDictionaryへ変換する場合は、LINQのToDictionaryを使います。

C#
var users = new List<User>
{
new User { Id = 1, Name = "田中" },
new User { Id = 2, Name = "鈴木" }
};

Dictionary<int, User> userDictionary =
users.ToDictionary(
user => user.Id,
user => user
);

キーだけを指定することもできます。

C#
var userDictionary = users.ToDictionary(
user => user.Id
);

生成されるキーが重複している場合はArgumentExceptionが発生します。Microsoft Learn

重複するデータが含まれる可能性がある場合は、事前にグループ化するなどの処理が必要です。

C#
var userDictionary = users
.GroupBy(user => user.Id)
.ToDictionary(
group => group.Key,
group => group.First()
);

8-5. Dictionaryを配列やListに変換する

Dictionary全体を配列に変換するには、ToArrayを使います。

C#
KeyValuePair<string, string>[] array =
products.ToArray();

Listへ変換する場合はToListを使います。

C#
List<KeyValuePair<string, string>> list =
products.ToList();

キーだけを変換することもできます。

C#
List<string> keys = products.Keys.ToList();

値だけを変換する場合は次のとおりです。

C#
string[] values = products.Values.ToArray();

ToArrayToListを呼び出すと、その時点の要素を持つ新しいコレクションが作成されます。

9. Dictionaryを使うときの注意点

9-1. Dictionaryではキーを重複させられない

Dictionaryのキーは一意でなければなりません。

C#
var scores = new Dictionary<string, int>
{
{ "田中", 90 },
{ "田中", 80 }
};

このコードは初期化時に例外が発生します。

同じ人物の複数の点数を管理したい場合は、値をList<int>にします。

C#
var scores = new Dictionary<string, List<int>>
{
{ "田中", new List<int> { 90, 80 } }
};

キーの比較方法は、コンストラクターに指定した比較子またはEqualityComparer<TKey>.Defaultによって決まります。

9-2. キーにnullを使用できない

Dictionaryのキーにnullは使用できません。

C#
var data = new Dictionary<string, string>();

data.Add(null!, "値");

実行するとArgumentNullExceptionが発生します。

値については、参照型やnull許容型であればnullを登録できます。

C#
var data = new Dictionary<string, string?>
{
{ "message", null }
};

キーに空文字列を使用することはできますが、データ設計として適切かどうかは別途判断しましょう。

9-3. 要素の並び順に依存しない

現在の.NET実装では、追加順に列挙されるように見える場合があります。しかし、Dictionaryの公式な契約では列挙順は保証されていません。Microsoft Learn

そのため、次のように順番を前提とした処理は避けましょう。

C#
var first = products.First();

キー順が必要なら、明示的に並び替えます。

C#
var orderedProducts = products.OrderBy(x => x.Key);

常にキー順で保持したい場合は、SortedDictionary<TKey, TValue>SortedList<TKey, TValue>を検討します。

9-4. 独自クラスをキーにするときは等価性を正しく定義する

独自クラスをキーにする場合、EqualsGetHashCodeが適切に実装されていないと、同じ内容のオブジェクトでも別のキーとして扱われます。

C#
public sealed class ProductKey
{
public string Category { get; }
public int Number { get; }

public ProductKey(string category, int number)
{
Category = category;
Number = number;
}

public override bool Equals(object? obj)
{
return obj is ProductKey other &&
Category == other.Category &&
Number == other.Number;
}

public override int GetHashCode()
{
return HashCode.Combine(Category, Number);
}
}

より簡潔にするなら、recordが便利です。

C#
public sealed record ProductKey(
string Category,
int Number
);

キーとして登録した後に、ハッシュ値へ影響するプロパティを変更してはいけません。検索できなくなる原因になります。Microsoft Learn

9-5. 複数スレッドから操作するときはConcurrentDictionaryを検討する

通常のDictionaryは、複数スレッドから同時に読み書きする用途には適していません。

複数スレッドで追加・更新・削除を行う場合は、ConcurrentDictionary<TKey, TValue>を検討します。

C#
using System.Collections.Concurrent;

var counts = new ConcurrentDictionary<string, int>();

counts.AddOrUpdate(
"apple",
1,
(_, current) => current + 1
);

ConcurrentDictionaryは、複数のスレッドから同時にアクセスできるスレッドセーフなキーと値のコレクションです。Microsoft Learn

ただし、複数の操作をまとめた処理全体が自動的に不可分になるわけではありません。GetOrAddAddOrUpdateなど、用途に合うメソッドを使用しましょう。

9-6. 検索速度とメモリ使用量の特徴を理解する

Dictionaryは、キーによる検索がO(1)に近く、高速であることが大きな利点です。Microsoft Learn

一方、ハッシュ値や内部の管理領域が必要なため、配列やListより多くのメモリを使用する傾向があります。

また、次の処理はキー検索と同じ速度ではありません。

  • ContainsValueによる値検索

  • LINQによる条件検索

  • 値を基準にした並び替え

  • 全要素の列挙

要素数が少ない場合は差を意識しすぎる必要はありません。大量データを扱う場合は、検索条件と更新頻度に合ったコレクションを選択しましょう。

10. Dictionary以外の連想配列と使い分け

10-1. SortedDictionaryでキー順に要素を管理する

SortedDictionary<TKey, TValue>は、キーの順番を保ちながら要素を管理します。

C#
var scores = new SortedDictionary<int, string>
{
{ 30, "C" },
{ 10, "A" },
{ 20, "B" }
};

foreach (var item in scores)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

出力はキー順になります。

10: A
20: B
30: C

SortedDictionaryは二分探索木で実装され、検索・追加・削除は基本的にO(log n)です。並び順が不要で検索速度を優先する場合はDictionary、常にキー順で扱いたい場合はSortedDictionaryが候補になります。Microsoft Learn

10-2. SortedListで省メモリと並び順を両立する

SortedList<TKey, TValue>もキー順にデータを保持します。

C#
var scores = new SortedList<int, string>
{
{ 30, "C" },
{ 10, "A" },
{ 20, "B" }
};

SortedDictionaryとの主な違いは内部構造です。

SortedListは配列を利用するため、一般にSortedDictionaryよりメモリ使用量が少なくなります。一方、並び順の途中へ要素を追加・削除すると、要素の移動が必要になるためO(n)になります。Microsoft Learn

要素の追加や削除が少なく、検索や列挙が中心の場合に適しています。

10-3. ConcurrentDictionaryでスレッドセーフに操作する

ConcurrentDictionary<TKey, TValue>は、複数スレッドからのアクセスを想定した連想配列です。

C#
var cache =
new ConcurrentDictionary<string, string>();

string value = cache.GetOrAdd(
"user:1",
_ => "田中"
);

既存値を更新する場合はAddOrUpdateを利用できます。

C#
cache.AddOrUpdate(
"user:1",
"田中",
(_, _) => "田中太郎"
);

並行処理を行わない通常のアプリケーションでは、より単純なDictionaryで十分です。

10-4. HashtableとDictionaryの違い

Hashtableは、ジェネリックが導入される前から存在する古い連想配列です。

C#
using System.Collections;

var table = new Hashtable();

table["name"] = "田中";
table[100] = DateTime.Now;

キーや値をobject型として扱うため、異なる型を混在させられます。しかし、型安全性が低く、値型ではボックス化が発生する場合があります。

C#
int value = (int)table["count"]!;

通常のC#コードでは、型安全で扱いやすいDictionary<TKey, TValue>を優先しましょう。

10-5. IReadOnlyDictionaryで読み取り専用として公開する

外部のコードからDictionaryを変更させたくない場合は、IReadOnlyDictionary<TKey, TValue>として公開できます。

C#
public class ProductService
{
private readonly Dictionary<string, string> _products =
new()
{
{ "P001", "ノートパソコン" },
{ "P002", "マウス" }
};

public IReadOnlyDictionary<string, string> Products
=> _products;
}

呼び出し側は、インターフェイス経由で追加や削除を行えません。

C#
IReadOnlyDictionary<string, string> products =
service.Products;

Console.WriteLine(products["P001"]);

ただし、元のDictionaryが内部で変更されると、その変更は読み取り側にも反映されます。完全に変更不可能なスナップショットが必要な場合は、ImmutableDictionaryFrozenDictionaryなども検討します。

10-6. 用途別に最適な連想配列を選ぶ基準

用途ごとの目安は次のとおりです。

要件適した型
高速なキー検索Dictionary
常にキー順で列挙SortedDictionary
キー順と省メモリを重視SortedList
複数スレッドから更新ConcurrentDictionary
読み取り専用として公開IReadOnlyDictionary
古いAPIとの互換性Hashtable
1つのキーに複数の値Dictionary<TKey, List<T>>またはLookup

特別な要件がなければ、まずDictionary<TKey, TValue>を選び、必要に応じて別のコレクションへ変更するとよいでしょう。

11. Dictionaryの実践的なサンプルコード

11-1. 商品コードと商品名を管理する

商品コードから商品名を検索する例です。

C#
var products = new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase)
{
{ "P001", "ノートパソコン" },
{ "P002", "マウス" },
{ "P003", "キーボード" }
};

Console.Write("商品コードを入力してください: ");
string code = Console.ReadLine() ?? "";

if (products.TryGetValue(code, out var productName))
{
Console.WriteLine($"商品名: {productName}");
}
else
{
Console.WriteLine("商品が見つかりません。");
}

大文字と小文字を区別しない比較子を指定しているため、p001と入力しても検索できます。

11-2. ユーザーIDからユーザー情報を検索する

ユーザー情報を独自クラスで管理する例です。

C#
public sealed class User
{
public int Id { get; init; }
public string Name { get; init; } = "";
public string Email { get; init; } = "";
}
C#
var users = new Dictionary<int, User>
{
{
1001,
new User
{
Id = 1001,
Name = "田中",
Email = "tanaka@example.com"
}
},
{
1002,
new User
{
Id = 1002,
Name = "鈴木",
Email = "suzuki@example.com"
}
}
};

int userId = 1002;

if (users.TryGetValue(userId, out var user))
{
Console.WriteLine($"名前: {user.Name}");
Console.WriteLine($"メール: {user.Email}");
}
else
{
Console.WriteLine("ユーザーが見つかりません。");
}

11-3. 単語の出現回数をカウントする

文章中の単語が何回登場したかを数える例です。

C#
string text = "apple orange apple banana orange apple";

var wordCounts = new Dictionary<string, int>(
StringComparer.OrdinalIgnoreCase
);

foreach (string word in text.Split(' '))
{
if (wordCounts.TryGetValue(word, out int count))
{
wordCounts[word] = count + 1;
}
else
{
wordCounts[word] = 1;
}
}

foreach (var item in wordCounts)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}

出力例は次のとおりです。

apple: 3
orange: 2
banana: 1

CollectionsMarshalなどを使った高度な最適化方法もありますが、通常はこの書き方が安全で理解しやすいでしょう。

11-4. 設定値をキーと値で管理する

アプリケーションの設定値を管理する例です。

C#
var settings = new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase)
{
{ "Language", "ja" },
{ "Theme", "dark" },
{ "PageSize", "20" }
};

string language = settings.GetValueOrDefault(
"Language",
"en"
);

int pageSize = int.TryParse(
settings.GetValueOrDefault("PageSize"),
out int parsedPageSize)
? parsedPageSize
: 10;

Console.WriteLine($"言語: {language}");
Console.WriteLine($"表示件数: {pageSize}");

すべてを文字列で管理すると変換処理が必要です。設定項目が固定されている場合は、専用の設定クラスを作る方法も検討しましょう。

11-5. Dictionaryの値にListを持たせて複数データを管理する

カテゴリごとに複数の商品を管理する例です。

C#
var categoryProducts =
new Dictionary<string, List<string>>();

void AddProduct(string category, string productName)
{
if (!categoryProducts.TryGetValue(
category,
out var products))
{
products = new List<string>();
categoryProducts[category] = products;
}

products.Add(productName);
}

AddProduct("パソコン", "ノートパソコン");
AddProduct("パソコン", "デスクトップパソコン");
AddProduct("周辺機器", "マウス");

foreach (var (category, products) in categoryProducts)
{
Console.WriteLine($"【{category}】");

foreach (string product in products)
{
Console.WriteLine($"- {product}");
}
}

1つのキーに複数の値を関連付けたい場合に、Dictionary<TKey, List<TValue>>はよく使われます。

12. C#の連想配列に関するよくある質問

12-1. Dictionaryのキーや値には異なる型を混在できる?

通常のDictionary<TKey, TValue>では、キーと値の型は宣言時に決まります。

C#
var data = new Dictionary<string, int>();

この場合、キーはstring、値はintだけです。

異なる型を混在させるためにobjectを指定することはできます。

C#
var data = new Dictionary<string, object>
{
{ "name", "田中" },
{ "age", 30 },
{ "isActive", true }
};

ただし、取り出すときに型の確認や変換が必要です。

C#
if (data["age"] is int age)
{
Console.WriteLine(age);
}

型安全性が下がるため、項目が決まっているなら専用クラスを作成するほうが適切です。

12-2. Dictionaryに同じ値を複数登録できる?

同じ値は複数登録できます。

C#
var users = new Dictionary<int, string>
{
{ 1, "田中" },
{ 2, "田中" }
};

重複できないのはキーです。

値も一意にしたい場合は、追加前にContainsValueで確認できます。

C#
if (!users.ContainsValue("鈴木"))
{
users.Add(3, "鈴木");
}

ただし、値の検索はO(n)です。頻繁に一意性を確認する場合は、HashSet<T>の併用などを検討してください。

12-3. 存在しないキーからデフォルト値を取得するには?

GetValueOrDefaultを使用すると、キーがない場合に既定値を取得できます。

C#
var scores = new Dictionary<string, int>
{
{ "田中", 90 }
};

int score = scores.GetValueOrDefault("鈴木");

Console.WriteLine(score); // 0

独自のデフォルト値も指定できます。

C#
int score = scores.GetValueOrDefault("鈴木", -1);

キーが存在したかどうかも区別したい場合は、TryGetValueを使用します。

C#
if (!scores.TryGetValue("鈴木", out int score))
{
score = -1;
}

値として0が正しく登録される可能性がある場合は、TryGetValueのほうが存在の有無を明確に判定できます。

12-4. Dictionaryをキー順・値順に並び替えるには?

キー順に並び替える場合はOrderByを使用します。

C#
var orderedByKey = products.OrderBy(x => x.Key);

降順の場合はOrderByDescendingです。

C#
var orderedByKeyDescending =
products.OrderByDescending(x => x.Key);

値順に並び替える場合は、Valueを指定します。

C#
var orderedByValue =
products.OrderBy(x => x.Value);

並び替えた結果を新しいDictionaryに変換することもできます。

C#
var orderedDictionary = products
.OrderBy(x => x.Key)
.ToDictionary(x => x.Key, x => x.Value);

ただし、Dictionaryの列挙順に依存する設計は避けるべきです。並び順を常に維持したい場合はSortedDictionaryを使用しましょう。

12-5. DictionaryをJSONに変換するには?

System.Text.JsonJsonSerializerを使用すると、DictionaryをJSONへ変換できます。

C#
using System.Text.Json;

var products = new Dictionary<string, string>
{
{ "P001", "ノートパソコン" },
{ "P002", "マウス" }
};

string json = JsonSerializer.Serialize(products);

Console.WriteLine(json);

出力例は次のとおりです。

JSON
{"P001":"ノートパソコン","P002":"マウス"}

読みやすく整形する場合は、オプションを指定します。

C#
var options = new JsonSerializerOptions
{
WriteIndented = true
};

string json = JsonSerializer.Serialize(
products,
options
);

JSONからDictionaryへ戻すこともできます。

C#
Dictionary<string, string>? restored =
JsonSerializer.Deserialize<
Dictionary<string, string>
>(json);

System.Text.Jsonでは、ディクショナリはJSONオブジェクトとしてシリアル化されます。Microsoft Learn

12-6. DictionaryとListはどちらを使うべき?

キーから要素を検索するならDictionary、順番や位置を基準に扱うならListが適しています。

Listが向いている例は次のとおりです。

C#
var tasks = new List<string>
{
"設計",
"実装",
"テスト"
};

Console.WriteLine(tasks[0]);

Dictionaryが向いている例は次のとおりです。

C#
var users = new Dictionary<int, string>
{
{ 1001, "田中" },
{ 1002, "鈴木" }
};

Console.WriteLine(users[1002]);

次の基準で判断するとよいでしょう。

  • 要素を順番に扱う:List

  • 数値インデックスで取得する:List

  • IDやコードで検索する:Dictionary

  • キーと値の対応を管理する:Dictionary

  • 同じキーに複数要素を持たせる:Dictionary<TKey, List<T>>

まとめ

C#で連想配列を使いたい場合は、Dictionary<TKey, TValue>を使用します。

Dictionaryでは、文字列や数値などのキーと値をセットで管理でき、キーから対応する値を高速に検索できます。

要素の追加には、重複時に例外を発生させるAdd、追加と更新を行えるインデクサー、重複を安全に判定できるTryAddを使い分けます。値を安全に取得する場合は、キーの存在確認と取得を同時に行えるTryGetValueが便利です。

削除にはRemove、全削除にはClearを使用します。繰り返し処理では、foreachによってKeyValuePair<TKey, TValue>としてキーと値を取得できます。

基本的な操作は次のコードに集約できます。

C#
var products = new Dictionary<string, string>();

// 追加
products.Add("P001", "ノートパソコン");
products.TryAdd("P002", "マウス");

// 追加または更新
products["P001"] = "ゲーミングノートパソコン";

// 検索
if (products.TryGetValue("P001", out var productName))
{
Console.WriteLine(productName);
}

// 繰り返し
foreach (var (code, name) in products)
{
Console.WriteLine($"{code}: {name}");
}

// 削除
products.Remove("P002");

// 全削除
products.Clear();

商品コード、ユーザーID、設定名など、識別可能なキーからデータを取得したい場面では、Dictionary<TKey, TValue>を活用しましょう。