本文へスキップ
ウェブエンジニア問題集
第8章

C#のコレクション — ListとDictionaryの使い分けと主要メソッド

約8分
この章の目次開く

複数のデータをまとめて扱う型を、まとめてコレクションと呼びます。JavaScriptでは配列とオブジェクト(とMap・Set)でほぼ足りますが、C#では用途ごとに専用の型を選ぶのが普通です。

選び方さえ分かれば難しくありません。この章では、実務で使う4つを中心に整理します。

まず選ぶべき4つ

型何を入れるか向いている操作JavaScriptで近いもの
T[](配列)同じ型の値を固定長で要素数が変わらない場面Array
List<T>同じ型の値を可変長で追加・削除・順番の保持Array
Dictionary<TKey, TValue>キーと値の組キーによる高速な検索Map
HashSet<T>重複しない値重複排除・存在確認Set
迷ったら List。キーで引きたいなら Dictionary、重複を許したくないなら HashSet です。
int[] scores = { 80, 92, 75 };
 
var names = new List<string> { "田中", "鈴木" };
 
var ages = new Dictionary<string, int>
{
    ["田中"] = 30,
    ["鈴木"] = 25
};
 
var tags = new HashSet<string> { "C#", ".NET", "C#" };  // 要素数は2
csharp

<T> の部分は「中に入る型」を指定するもので、ジェネリクスと呼ばれます。List<string> は文字列専用のリストであり、int を追加しようとするとコンパイルエラーになります。仕組みは C#のジェネリクス で扱います。

Listの主要メソッド

構文: list.Add(item)

引数渡せるもの説明
item(第1引数)リストの型と同じ値末尾に追加する要素

戻り値: なし(void)

主要な操作を一覧にします。

メソッド引数戻り値
Add(item)追加する要素なし
Insert(index, item)位置と要素なし
Remove(item)削除したい要素削除できたかの bool
RemoveAt(index)位置なし
RemoveAll(predicate)条件を表すラムダ式削除した件数 int
Contains(item)探す要素含まれるかの bool
IndexOf(item)探す要素位置。見つからなければ -1
Clear()なしなし
var names = new List<string> { "田中", "鈴木", "佐藤" };
 
names.Add("高橋");
names.Remove("鈴木");
Console.WriteLine(names.Count);          // 3
Console.WriteLine(names.Contains("佐藤"));  // True
Console.WriteLine(names[0]);             // 田中
csharp

要素数は Length ではなく Count です(配列だけ Length)。ここは最初によく間違えます。

一覧を確認するイメージ
要素数は List なら Count、配列なら Length と覚えます

Dictionaryの主要メソッド

キーで値を引く用途にはDictionaryを使います。存在しないキーで取り出すと例外になるため、TryGetValue を使うのが基本です。

構文: dict.TryGetValue(key, out value)

引数渡せるもの説明
key(第1引数)キーの型と同じ値探したいキー
value(第2引数)out 付きの変数見つかった場合に値が入る

戻り値: キーが存在したかの bool(存在しなければ value は既定値)

var ages = new Dictionary<string, int> { ["田中"] = 30 };
 
if (ages.TryGetValue("鈴木", out var age))
{
    Console.WriteLine(age);
}
else
{
    Console.WriteLine("登録されていません");
}
csharp
メソッド引数戻り値
Add(key, value)キーと値なし(キー重複で例外)
TryAdd(key, value)キーと値追加できたかの bool
TryGetValue(key, out value)キーと受け取る変数存在したかの bool
ContainsKey(key)キー存在するかの bool
Remove(key)キー削除できたかの bool
学習者学習者

JavaScriptのオブジェクトみたいに、無いキーを読んだら undefined が返る……わけではないんですね。

C#では例外(KeyNotFoundException)になります。値が無いことを想定するなら TryGetValue、キーの有無だけ知りたいなら ContainsKey を使います。この「失敗を戻り値で表す」形は C#のメソッド で見た out パターンそのものです。

HashSetと重複排除

var tags = new HashSet<string>();
Console.WriteLine(tags.Add("C#"));   // True(追加された)
Console.WriteLine(tags.Add("C#"));   // False(既にある)
csharp

Contains の速さが List と大きく違います。要素数が増えるほど差が開くため、「含まれているか」を大量に判定するなら HashSet を選びます。

型要素の検索順序
List<T>先頭から順に探す(要素数に比例)追加順を保持
HashSet<T>ハッシュで一発保証されない
Dictionary<TKey, TValue>ハッシュで一発保証されない

コレクションを型で受け渡す

メソッドの引数では、具体的な型ではなくインターフェースで受け取ると、呼び出し側の自由度が上がります。

受け取る型できること使いどころ
IEnumerable<T>順に読むだけ読むだけのメソッドの引数
IReadOnlyList<T>読む・件数・添字変更させたくない公開API
IList<T>追加・削除・添字中身を変更する処理
List<T>すべてローカル変数・内部実装
public int Total(IEnumerable<int> values)
{
    int sum = 0;
    foreach (var v in values) sum += v;
    return sum;
}
csharp

こう書いておくと、配列でもリストでもLINQの結果でもそのまま渡せます。

よくあるハマりどころ

Count と Length を取り違える

List<T> は Count、配列は Length、string も Length です。LINQを使うと Count() というメソッドもあり紛らわしいですが、プロパティの Count があるならそちらを使います。

存在しないインデックスにアクセスする

var list = new List<int> { 1, 2, 3 };
// var x = list[3];  ← ArgumentOutOfRangeException
csharp

件数は list.Count、最後の要素は list[^1](末尾からの添字)で取得できます。

ループ中に要素を削除する

foreach の途中で Add や Remove を呼ぶと実行時例外になります。条件に合う要素をまとめて消したいなら RemoveAll を使います(C#の条件分岐と繰り返し 参照)。

空のリストと null を混同する

「要素が無い」を null で表すと、使う側が毎回nullチェックを強いられます。返すものが無い場合は空のリストを返すのが基本です。

先生先生

return new List<User>(); を返すだけで、呼び出し側の foreach はそのまま安全に回せるからね。

ちゃんと使うためのポイント

  • 基本は List<T>、キー検索は Dictionary、重複排除は HashSet
  • 要素数は List なら Count、配列なら Length
  • Dictionaryから値を取るときは TryGetValue
  • Add は重複で例外、インデクサ代入は上書き
  • メソッドの引数は IEnumerable<T> などのインターフェースで受け取る
  • 「結果なし」は null ではなく空のコレクションで返す

次の章では、これらのコレクションを宣言的に操作するLINQに進みます。ここからC#らしい書き味になります。

参考リンク