C# でデータをまとめて管理するとき、最初に選択肢として挙がるのが 配列(array) と List<T> です。「どちらでも似たようなことができる」と思われがちですが、設計上の役割が異なり、使い分けを間違えるとパフォーマンスの無駄やバグの原因になります。
本記事では2つの構造の内部的な違いから、多次元配列・ジャグ配列・主要メソッド・相互変換・インターフェース、そして Dictionary・HashSet・Queue・Stack を含むコレクション型全体の選択指針まで解説します。
配列(array)の基本と特徴
配列は宣言時にサイズを固定した、同じ型の要素を連続したメモリ領域に格納するコレクションです。
// サイズを指定して宣言(全要素がデフォルト値で初期化)
int[] numbers = new int[5]; // { 0, 0, 0, 0, 0 }
// 初期値を指定して宣言
string[] fruits = { "Apple", "Banana", "Orange" };
// new[] で型推論
var scores = new[] { 85, 92, 78, 64 };
// 要素へのアクセスは 0 始まりのインデックス
Console.WriteLine(fruits[0]); // Apple
Console.WriteLine(fruits[^1]); // Orange(C# 8以降: 末尾から1番目)
Console.WriteLine(fruits.Length); // 3(要素数)
int[] data = { 10, 20, 30, 40, 50 };
// C# 8以降: Range でスライス
int[] slice1 = data[1..4]; // { 20, 30, 40 }(1〜3番目)
int[] slice2 = data[2..]; // { 30, 40, 50 }(2番目以降)
int[] slice3 = data[..3]; // { 10, 20, 30 }(先頭から3つ)
// 末尾インデックス(^ 演算子)
int last = data[^1]; // 50
int second = data[^2]; // 40
・サイズは宣言時に確定し、後から変更できない
・連続したメモリ領域に格納されるため、インデックスアクセスが O(1) で非常に高速
・すべての型(値型・参照型)に対応。
int[] は値型配列でヒープに直接格納される・
System.Array クラスを継承しており、Sort・Find・Copy などの静的メソッドが使える
多次元配列とジャグ配列
多次元配列(行列)
行列のような2次元データには 多次元配列 を使います。
// 3行4列の2次元配列
int[,] matrix = new int[3, 4];
// 初期値付きで宣言
int[,] grid = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
Console.WriteLine(grid[1, 2]); // 6(2行目・3列目)
Console.WriteLine(grid.GetLength(0)); // 3(行数)
Console.WriteLine(grid.GetLength(1)); // 3(列数)
ジャグ配列(配列の配列)
ジャグ配列(jagged array)は行ごとに長さが異なる配列です。行列と異なり「行によって列数が違う」データに向いています。
// 行ごとにサイズが異なる配列の配列
int[][] jagged = new int[3][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5, 6 };
jagged[2] = new int[] { 7 };
foreach (var row in jagged)
{
Console.WriteLine(string.Join(", ", row));
}
// 1, 2
// 3, 4, 5, 6
// 7
| 種類 | 構造 | 特徴 | 使いどころ |
|---|---|---|---|
多次元配列 int[,] |
行×列が固定の行列 | メモリ連続・高速 | 数学的な行列・画像ピクセル・テーブルデータ |
ジャグ配列 int[][] |
行ごとに長さが違う | 柔軟・LINQ で扱いやすい | 可変長の行データ・グラフの隣接リスト |
Array クラスの便利なメソッド
int[] nums = { 5, 2, 8, 1, 9, 3 };
// ソート
Array.Sort(nums);
Console.WriteLine(string.Join(", ", nums)); // 1, 2, 3, 5, 8, 9
// 逆順
Array.Reverse(nums);
Console.WriteLine(string.Join(", ", nums)); // 9, 8, 5, 3, 2, 1
// 検索(ソート済み配列に対して二分探索)
Array.Sort(nums);
int index = Array.BinarySearch(nums, 5);
Console.WriteLine(index); // 3(0始まり)
// 線形検索(ラムダで条件指定)
int found = Array.Find(nums, n => n > 7);
Console.WriteLine(found); // 8(最初にヒットした要素)
// コピー
int[] copy = new int[nums.Length];
Array.Copy(nums, copy, nums.Length);
// 全要素を同じ値で埋める
Array.Fill(copy, 0);
Console.WriteLine(string.Join(", ", copy)); // 0, 0, 0, 0, 0, 0
List<T> の基本と特徴
List<T> は内部に配列を持ち、容量が不足したときに自動的に拡張するコレクションです。要素の追加・削除が自由にできます。
using System.Collections.Generic;
// 空のリストを作成
List<string> names = new List<string>();
// 初期値付きで作成
List<int> scores = new List<int> { 85, 92, 78 };
// C# 12 以降: コレクション式(どのコレクション型にも統一的に使える)
List<string> fruits = ["Apple", "Banana", "Orange"];
Console.WriteLine(scores.Count); // 3(要素数)
Console.WriteLine(scores[0]); // 85
List<T> の主要メソッド
var list = new List<string> { "A", "B", "C" };
// 末尾に追加
list.Add("D"); // [A, B, C, D]
// 複数追加
list.AddRange(new[] { "E", "F" }); // [A, B, C, D, E, F]
// 指定位置に挿入
list.Insert(1, "X"); // [A, X, B, C, D, E, F]
// 値で削除(最初の1件)
list.Remove("X"); // [A, B, C, D, E, F]
// インデックスで削除
list.RemoveAt(0); // [B, C, D, E, F]
// 条件に合致する全要素を削除
list.RemoveAll(s => s == "C" || s == "D"); // [B, E, F]
// 全削除
list.Clear();
var items = new List<string> { "Banana", "Apple", "Cherry", "Apple" };
// 存在確認
bool has = items.Contains("Apple"); // true
// インデックス取得
int first = items.IndexOf("Apple"); // 1
int last = items.LastIndexOf("Apple"); // 3
// 条件で検索
string? found = items.Find(s => s.StartsWith("C")); // "Cherry"
List<string> all = items.FindAll(s => s.Length > 5); // ["Banana", "Cherry"]
// ソート(デフォルト: 昇順)
items.Sort();
Console.WriteLine(string.Join(", ", items)); // Apple, Apple, Banana, Cherry
// カスタムソート(ラムダで比較)
items.Sort((a, b) => b.CompareTo(a)); // 降順
Console.WriteLine(string.Join(", ", items)); // Cherry, Banana, Apple, Apple
var nums = new List<int> { 1, 2, 3, 4, 5, 6 };
// 指定範囲を別 List として取得
List<int> sub = nums.GetRange(1, 3); // [2, 3, 4](index=1 から 3個)
// 配列に変換
int[] arr = nums.ToArray();
// 特定条件に合致するか確認
bool anyOver5 = nums.Exists(n => n > 5); // true
bool allOver0 = nums.TrueForAll(n => n > 0); // true
// 事前に容量を確保してパフォーマンス改善
var large = new List<int>(capacity: 10000);
List<T> の内部は配列で、容量不足時に約2倍に拡張します。大量データを追加することがわかっているときは new List<T>(capacity: 予測件数) で初期容量を指定すると、再アロケーションのコストを削減できます。配列 vs List の比較
| 項目 | 配列(array) | List<T> |
|---|---|---|
| サイズ | 固定(変更不可) | 可変(自動拡張) |
| 要素の追加・削除 | 不可(Array.Resize で別配列生成) | Add/Remove/Insert |
| インデックスアクセス | O(1)・最速 | O(1)・ほぼ同等 |
| 末尾への追加 | Array.Resize で O(n) | Add で 償却 O(1) |
| 中間への挿入・削除 | 不可(手動コピー必要) | O(n)(後続要素をシフト) |
| 検索(インデックス未知) | Array.Find / BinarySearch | Find / BinarySearch |
| メモリ効率 | ヘッダーのみのオーバーヘッド | Capacity 分の余分メモリを確保 |
| 型安全 | 型指定必須 | ジェネリクスで型安全 |
| LINQ | 対応(IEnumerable |
対応(IEnumerable |
| マルチスレッド | 非スレッドセーフ(変更しなければ安全) | 非スレッドセーフ |
配列と List の相互変換
// 配列 → List
int[] arr = { 1, 2, 3, 4, 5 };
List<int> list = arr.ToList(); // LINQ の拡張メソッド
List<int> list2 = new List<int>(arr); // コンストラクタでも可
// List → 配列
int[] back = list.ToArray();
// Span<T> で無コピーのスライス(パフォーマンス重視の場合)
Span<int> span = arr.AsSpan(1, 3); // { 2, 3, 4 }(コピーなし)
foreach (var n in span)
Console.Write($"{n} "); // 2 3 4
Span<T> は配列やメモリの一部を参照するための構造体です。スライスしてもコピーが発生しないため、大きな配列の部分処理にゼロアロケーションで対応できます。ただしヒープには置けないためクラスのフィールドには使えません。読み取り専用の場合は ReadOnlySpan<T> を使います。
インターフェースを使った型宣言
配列も List<T> もいくつかの共通インターフェースを実装しています。変数をインターフェース型で宣言することで、実装を隠蔽したり、呼び出し元が具体的な型に依存しないコードを書けます。
// IEnumerable<T>: foreach でのみ使う読み取り専用の列挙
IEnumerable<int> seq = new List<int> { 1, 2, 3 };
// IReadOnlyList<T>: 読み取り専用・インデックスアクセス可・Count あり
IReadOnlyList<string> readOnly = new List<string> { "A", "B" };
Console.WriteLine(readOnly[0]); // A
Console.WriteLine(readOnly.Count); // 2
// IList<T>: 読み書き可能・インデックスアクセス・Add/Remove あり
IList<double> mutable = new double[] { 1.0, 2.0, 3.0 }; // 配列も IList<T>
| インターフェース | 提供する機能 | 注意点 |
|---|---|---|
IEnumerable<T> |
foreach だけ使う・LINQ のベース | インデックスアクセス不可 |
IReadOnlyCollection<T> |
Count + 列挙 | Add/Remove/インデックス不可 |
IReadOnlyList<T> |
Count + 列挙 + インデックスアクセス | Add/Remove 不可 |
IList<T> |
読み書き + インデックスアクセス + Add/Remove | 全機能(配列も実装するがAdd/Removeは例外) |
ICollection<T> |
Count + Add/Remove + Contains | インデックスアクセス不可 |
IEnumerable<T>、「インデックスアクセスが必要」なら IReadOnlyList<T> にすると、呼び出し元に実装の変更が影響しにくくなります。コレクション型の選択ガイド
配列・List 以外にも状況に合ったコレクション型があります。選択の指針をまとめます。
| ニーズ | 推奨コレクション型 | 代表例 |
|---|---|---|
| 要素数が固定・高速アクセス重視 | T[](配列) |
固定長の数値データ・バイト配列 |
| 要素の追加・削除が必要 | List<T> |
可変長のデータリスト |
| キーで高速検索したい | Dictionary<TKey, TValue> |
名前→値のマッピング・キャッシュ |
| 重複なしの集合・集合演算したい | HashSet<T> |
タグのユニーク管理・集合の和・積 |
| 先入れ先出し(FIFO) | Queue<T> |
タスクキュー・メッセージ処理 |
| 後入れ先出し(LIFO) | Stack<T> |
Undo履歴・深さ優先探索 |
| ソート済みを維持したい | SortedList<K,V> / SortedSet<T> |
順序付きデータ |
| マルチスレッドで共有 | ConcurrentQueue<T>などConcurrent系 |
スレッド安全が必要な場合 |
よくある落とし穴と注意点
配列はサイズ変更できない(Array.Resize は別オブジェクトを作る)
Array.Resize(ref arr, newSize) は既存の配列を拡張するのではなく、新しい配列を作って参照を差し替えます。元の配列への参照を他の変数が持っていた場合、そちらには変更が反映されません。頻繁にサイズが変わるなら最初から List<T> を使うべきです。
foreach 中に List を変更すると例外が発生する
foreach でループ中に List<T> から要素を追加・削除すると InvalidOperationException が発生します。削除対象を別リストに収集してループ後に RemoveAll で処理するか、for 文を逆順に使います。
List の Count と配列の Length は混同しやすい
配列の要素数は .Length、List<T> の要素数は .Count です。どちらも「要素数」を表しますが、プロパティ名が異なります。List<T> には Capacity(内部配列の確保サイズ)もあり、Count とは別物なので混同しないようにしましょう。
参照型の配列コピーはシャローコピーになる
Array.Copy や ToArray()・ToList() で作ったコピーはシャローコピーです。要素が参照型(クラス)の場合、コピー先と元で同一オブジェクトを指すため、コピー先の要素を変更すると元にも影響します。ディープコピーが必要な場合は要素ごとに new する必要があります。
多次元配列とジャグ配列の LINQ 扱いの違い
多次元配列(int[,])は IEnumerable<T> を直接実装していないため、LINQ メソッドをそのままかけられません。Cast<int>() で変換するか、ジャグ配列(int[][])を使うと SelectMany で平坦化して LINQ を活用できます。
よくある質問
List<T> を選ぶのが無難です。要素の追加・削除が容易で、LINQ との相性も良く、後からサイズ変更も可能です。配列を選ぶ理由は①要素数が完全に固定、②パフォーマンスクリティカルなコード、③APIの戻り値が配列で決まっている、といった明確な理由がある場合に限ります。T[] 配列を持ち、要素数がその容量を超えると約2倍のサイズで再アロケーションします。初期 Capacity は 0 で、最初の Add 時に 4 になり、以降 8・16・32 と倍々で拡張します。大量データを追加するとわかっている場合は new List<T>(capacity) で初期容量を指定してアロケーション回数を減らせます。int[] のように要素が値型でも、配列オブジェクト自体はヒープに確保されます。ただし int[] の場合、要素の値は配列オブジェクト内に直接格納されるため(ボックス化なし)、object[] の配列より効率的です。ArrayList は .NET 1.0 時代の非ジェネリクスコレクションで、全要素を object として格納します。取り出すたびにキャストが必要で、実行時エラーのリスクがあります。現代の C# では使う理由がなく、List<T> が完全な上位互換です。ArrayList が出てきたらすべて List<T> に置き換えてください。.ToList() で具体化すべきです。IEnumerable<T> は LINQ クエリの場合があり、アクセスのたびにクエリが再実行されることがあります。ただし1回だけ foreach するだけなら IEnumerable<T> のまま使うことでメモリを節約できます。まとめ
| 項目 | 配列(array) | List<T> |
|---|---|---|
| サイズ | 固定(変更不可) | 可変(自動拡張) |
| 追加・削除 | 不可 | Add / Remove / Insert |
| アクセス速度 | 最速(O(1)) | ほぼ同等(O(1)) |
| 要素数の取得 | .Length |
.Count |
| 多次元 | int[,] / int[][] |
対応なし(Listのネストで代替) |
| 変換 | .ToList() で List 化 |
.ToArray() で配列化 |
| インターフェース | IList<T> を実装(Add は例外) | IList<T> を完全実装 |
| 主な選択理由 | 固定長・パフォーマンス重視 | 可変長・汎用(迷ったらこちら) |
ループ処理と組み合わせた使い方はfor・foreach・whileのループ処理完全ガイド、LINQ を使った高度なコレクション操作はLINQの基本(Where・Select)も参照してください。

