【C#】配列とList完全ガイド|違い・使い分け・多次元配列・主要メソッド・コレクション型選択まで

【C#】配列とListの違いと使い分け C#

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次元データには 多次元配列 を使います。

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 クラスの便利なメソッド

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> は内部に配列を持ち、容量が不足したときに自動的に拡張するコレクションです。要素の追加・削除が自由にできます。

List の宣言と初期化
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 の変換
// 配列 → 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> とは
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 は混同しやすい

配列の要素数は .LengthList<T> の要素数は .Count です。どちらも「要素数」を表しますが、プロパティ名が異なります。List<T> には Capacity(内部配列の確保サイズ)もあり、Count とは別物なので混同しないようにしましょう。

参照型の配列コピーはシャローコピーになる

Array.CopyToArray()ToList() で作ったコピーはシャローコピーです。要素が参照型(クラス)の場合、コピー先と元で同一オブジェクトを指すため、コピー先の要素を変更すると元にも影響します。ディープコピーが必要な場合は要素ごとに new する必要があります。

多次元配列とジャグ配列の LINQ 扱いの違い

多次元配列(int[,])は IEnumerable<T> を直接実装していないため、LINQ メソッドをそのままかけられません。Cast<int>() で変換するか、ジャグ配列(int[][])を使うと SelectMany で平坦化して LINQ を活用できます。

よくある質問

Q配列と List はどちらを使うべきですか?
A迷ったら List<T> を選ぶのが無難です。要素の追加・削除が容易で、LINQ との相性も良く、後からサイズ変更も可能です。配列を選ぶ理由は①要素数が完全に固定、②パフォーマンスクリティカルなコード、③APIの戻り値が配列で決まっている、といった明確な理由がある場合に限ります。
QList の内部はどうなっていますか?
A内部に T[] 配列を持ち、要素数がその容量を超えると約2倍のサイズで再アロケーションします。初期 Capacity は 0 で、最初の Add 時に 4 になり、以降 8・16・32 と倍々で拡張します。大量データを追加するとわかっている場合は new List<T>(capacity) で初期容量を指定してアロケーション回数を減らせます。
Q配列は値型ですか?参照型ですか?
AC# の配列は参照型です。int[] のように要素が値型でも、配列オブジェクト自体はヒープに確保されます。ただし int[] の場合、要素の値は配列オブジェクト内に直接格納されるため(ボックス化なし)、object[] の配列より効率的です。
QArrayList と List はどう違いますか?
AArrayList は .NET 1.0 時代の非ジェネリクスコレクションで、全要素を object として格納します。取り出すたびにキャストが必要で、実行時エラーのリスクがあります。現代の C# では使う理由がなく、List<T> が完全な上位互換です。ArrayList が出てきたらすべて List<T> に置き換えてください。
QIEnumerable で受け取った引数を List に変換すべきですか?
A複数回ループしたり Count を取得したりする場合は .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)も参照してください。