System.Collections.Generic.List<T> は、C# でもっとも頻繁に使われるコレクションです。型安全な動的配列 として、配列のシンプルさとサイズの可変性を両立しています。本記事では内部実装、サポートインターフェース、性能特性、実務での使いどころを整理します。
List<T> とは
- 名前空間:
System.Collections.Generic - 内部構造:
T[]バッファ +_size(要素数)+Capacity(確保済み容量) - 容量超過時は 2 倍に拡張(実装は
T[Capacity * 2]を確保してArray.Copy) Tは参照型・値型どちらもボックス化なしで格納できる
var list = new List<int> { 1, 2, 3 };
list.Add(4);
list.Insert(0, 0); // 先頭挿入:O(n)
list.RemoveAt(2);
int x = list[1]; // インデックスアクセス:O(1)
サポートするインターフェース
| インターフェース | 役割 |
|---|---|
IList<T> |
インデックスアクセス・挿入・削除を持つ順序付きコレクション |
IReadOnlyList<T> |
インデックスアクセスのみ、書き換え不可ビュー |
ICollection<T> |
Count / Add / Remove / CopyTo 等 |
IReadOnlyCollection<T> |
読み取り専用の Count |
IEnumerable<T> |
foreach での列挙 |
IList / ICollection / IEnumerable |
非ジェネリック版(互換) |
各インターフェースの詳細は次を参照してください。
IList/ICollection: ArrayList の記事IEnumerable/IEnumerator: IEnumerable と IEnumerator の記事
IList<T> はジェネリックなインデックス付きコレクションを表します。
public interface IList<T> : ICollection<T>
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
API 境界には 読み取り専用なら IReadOnlyList<T>、書き換え可なら IList<T> を指定するのが基本。List<T> を直接公開すると外部が拡張・縮小できてしまうので避けましょう。
主な API と計算量
| 操作 | API | 計算量 | 備考 |
|---|---|---|---|
| インデックスアクセス | list[i] |
O(1) | |
| 末尾追加 | Add(item) |
O(1) 償却 | 容量超過時は O(n) |
| 範囲追加 | AddRange(items) |
O(k) | |
| 任意位置挿入 | Insert(i, item) |
O(n) | 後続要素を 1 つずらす |
| 任意位置削除 | RemoveAt(i) / Remove(item) |
O(n) | |
| 末尾削除 | RemoveAt(Count - 1) |
O(1) | |
| 検索 | Contains / IndexOf |
O(n) | 線形探索 |
| ソート | Sort() |
O(n log n) | 内部は Array.Sort |
| 二分探索 | BinarySearch(item) |
O(log n) | 事前に整列が必要 |
| 範囲削除 | RemoveAll(predicate) |
O(n) | フィルタリングを兼ねる |
Capacity と EnsureCapacity
要素数の見当が付くなら 必ず Capacity を指定 することで、再確保とコピーの回数を減らせます。
var list = new List<int>(capacity: 1_000_000);
for (int i = 0; i < 1_000_000; i++) list.Add(i);
.NET 6+ では EnsureCapacity(min) で動的に拡張できます。逆に縮める TrimExcess() も用意されています。
list.EnsureCapacity(2_000_000);
list.TrimExcess();
高効率アクセス — Span<T> と CollectionsMarshal
ホットパスで List<T> を回すなら、CollectionsMarshal.AsSpan(list) で内部配列に直接アクセスする Span<T> が取れます(.NET 5+)。境界チェックの省略やベクトル化と組み合わせて非常に高速です。
using System.Runtime.InteropServices;
Span<int> span = CollectionsMarshal.AsSpan(list);
for (int i = 0; i < span.Length; i++) span[i] *= 2;
ただし AsSpan 後にリストの構造を変更(Add/Insert/Remove)してはいけません。Span が無効な内部配列を指す可能性があります。
ボックス化はしない
List<int> は内部が int[] なので、int をそのまま格納します。object[] に詰める ArrayList のようなボックス化コストは発生しません。値型のコレクションは無条件に List<T> を選ぶのが鉄則。
配列・LinkedList<T> との比較
| 観点 | T[](配列) |
List<T> |
LinkedList<T> |
|---|---|---|---|
| サイズ | 固定 | 可変 | 可変 |
| インデックスアクセス | O(1) | O(1) | 非対応 |
| 末尾追加 | × | O(1) 償却 | O(1) |
| 任意位置挿入(位置既知) | × | O(n) | O(1)(ノード既知時) |
| メモリオーバーヘッド | 最小 | 配列+少量 | ノードあたり前後ポインタ |
| キャッシュ局所性 | 高 | 高 | 低 |
「サイズが固定 & 最高速」が要件なら配列、それ以外はほぼ List<T>。LinkedList<T> は LRU キャッシュなどの特殊な用途に限定されます。
使いどころ
List<T> は 「迷ったらこれ」 の汎用ツールです。
- 任意の集合の保持・列挙
- 配列より柔軟に伸縮させたい
- LINQ で射影・フィルタの起点にする
- API の戻り値で順序付き集合を返す(公開 API では
IReadOnlyList<T>で)
並行アクセスが必要なら ConcurrentBag<T> / ConcurrentQueue<T> / 自前ロック、要素数が極端に少なく頻繁に作って捨てるなら Span<T>/stackalloc を検討します。
イディオム
列挙中の削除
list.RemoveAll(x => x.IsExpired); // O(n) で済む。ループより高速
ループでの Remove は O(n²) になるので、RemoveAll か 逆順インデックスループ を使います。
for (int i = list.Count - 1; i >= 0; i--)
if (list[i].IsExpired) list.RemoveAt(i);
配列との相互変換
int[] arr = list.ToArray(); // 新しい配列にコピー
var copy = new List<int>(arr); // 配列から List を作る
ReadOnlySpan<int> span = CollectionsMarshal.AsSpan(list); // 内部参照
不変ビューの公開
public IReadOnlyList<Item> Items => _items; // 内部の List<Item> を読み取り専用で公開
外部から書き換えさせたくない場合の基本。完全な不変が必要なら ImmutableList<T> か ReadOnlyCollection<T>(AsReadOnly())を使います。
注意点
- 列挙中の変更は例外:
foreach中にAdd/Remove/Insertを呼ぶとInvalidOperationException。 - スレッドセーフではない:並行アクセスには別途ロック、または
ConcurrentBag<T>等を選択。 Capacity != Count:Countは要素数、Capacityは確保済み容量。TrimExcessで寄せられる。
まとめ
List<T>は C# のもっとも標準的な動的配列。インデックスアクセス O(1)、末尾追加 O(1) 償却。IList<T>/IReadOnlyList<T>/ICollection<T>/IEnumerable<T>等を実装。- 値型もボックス化なしで格納できる。
ArrayListは使わない。 - 容量を事前指定して再確保を抑える、
CollectionsMarshal.AsSpanで高効率に走査するなど、ホットパスでの最適化手段が用意されている。 - 「迷ったら
List<T>」。順序付き集合の事実上の標準。