C#におけるプログラミングにおいて、データの集合を扱う際に最も頻繁に利用されるのがList<T>クラスです。

配列と似た性質を持ちながら、要素の追加や削除に合わせてサイズを動的に変更できるという柔軟性を備えており、現代のC#開発では欠かすことのできない存在となっています。

本記事では、C#のListの基本的な使い方から、パフォーマンスを意識した初期化、要素の検索・並べ替え、そして強力なLINQを用いた高度な操作まで、初心者から中級者までが実戦で活用できる知識を網羅的に解説します。

List<T>とは?配列との違いと特徴

C#のList<T>は、System.Collections.Generic名前空間に属する可変長配列です。

ジェネリッククラスとして実装されているため、特定のデータ型を指定して安全にデータを保持することができます。

配列とListの主な違い

配列(Array)とListの最大の違いは、サイズの変更が可能かどうかです。

  1. 配列(Array): インスタンス化する際に要素数を決定する必要があり、後からサイズを変更することはできません。
  2. List<T>: 必要に応じて要素を自由に追加・削除でき、内部でメモリ管理が自動的に行われます。

また、配列は「低レベルでメモリ効率が良い」という利点がありますが、実務のアプリケーション開発においては、利便性の高いListが選ばれることが一般的です。

Listの初期化と基本的な宣言方法

Listを使用するには、まずList<T>のインスタンスを作成する必要があります。

C#のバージョンアップに伴い、初期化の方法も多様化しています。

基本的な初期化

最も標準的な初期化方法は、newキーワードを使用する方法です。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 空のリストを作成
        List<int> numbers = new List<int>();

        // 初期値を指定して作成(コレクション初期化子)
        List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };

        Console.WriteLine($"fruitsの要素数: {fruits.Count}");
    }
}
実行結果
fruitsの要素数: 3

C# 12以降のコレクション式(Collection Expressions)

最新のC#では、[](ブラケット)を用いたコレクション式による初期化が推奨されています。

コードが簡潔になり、可読性が向上します。

C#
// C# 12以降の書き方
List<int> scores = [80, 90, 75, 100];
List<string> emptyList = [];

キャパシティを指定した初期化

パフォーマンスを重視する場合、あらかじめ格納する要素数が予測できているのであれば、キャパシティ(容量)を指定して初期化することが有効です。

C#
// 最初から100個分のメモリ領域を確保する
List<int> largeData = new List<int>(100);

Listは要素が追加されるたびに内部配列を再確保しますが、あらかじめ領域を確保しておくことで、メモリの再割り当てコストを抑えることができます。

要素の追加と挿入

Listにデータを追加する方法には、末尾に追加するAddや、特定の場所を指定するInsertなどがあります。

AddとAddRange

単一の要素を追加する場合はAdd、複数の要素をまとめて追加する場合はAddRangeを使用します。

C#
List<string> names = ["Alice", "Bob"];

// 単一追加
names.Add("Charlie");

// まとめて追加(他のリストや配列を渡せる)
string[] additionalNames = ["Dave", "Eve"];
names.AddRange(additionalNames);

foreach (var name in names)
{
    Console.WriteLine(name);
}
実行結果
Alice
Bob
Charlie
Dave
Eve

InsertとInsertRange

特定のインデックスに要素を割り込ませたい場合は、Insertを使用します。

C#
List<string> colors = ["Red", "Blue"];

// インデックス1の位置に"Green"を挿入
colors.Insert(1, "Green");

// 結果: ["Red", "Green", "Blue"]

ただし、挿入操作はそれ以降の要素をすべて後ろにずらす必要があるため、要素数が多いリストの先頭付近への挿入は計算負荷が高いことに注意が必要です。

要素の削除とクリア

Listから要素を取り除くメソッドも豊富に用意されています。

Remove, RemoveAt, RemoveAll

  • Remove(value): 最初に見つかった指定の値を削除します。
  • RemoveAt(index): 指定した位置にある要素を削除します。
  • RemoveAll(predicate): 条件に一致する要素をすべて削除します。
C#
List<int> nums = [10, 20, 30, 20, 40];

// 値を指定して削除(最初の20が消える)
nums.Remove(20);

// インデックスを指定して削除(現在のインデックス0の10が消える)
nums.RemoveAt(0);

// 条件を指定して削除(30以上の数値をすべて消す)
nums.RemoveAll(n => n >= 30);

// 最後に残っている要素を表示
foreach (var n in nums)
{
    Console.WriteLine(n);
}
実行結果
20

Clear

リスト内のすべての要素を削除し、空の状態にするにはClearを使用します。

C#
nums.Clear();
Console.WriteLine($"要素数: {nums.Count}"); // 要素数: 0

要素の検索と存在確認

特定のデータがリストに含まれているかを調べる方法は、開発において非常に重要です。

ContainsとExists

もっとも単純な存在確認にはContainsを用います。

より複雑な条件で探したい場合はExistsが便利です。

C#
List<string> members = ["Sato", "Tanaka", "Suzuki"];

// 含まれているかチェック
bool hasSato = members.Contains("Sato"); // true

// "Su"で始まる名前があるかチェック
bool hasSu = members.Exists(m => m.StartsWith("Su")); // true

FindとFindIndex

条件に一致する要素そのものを取得したい場合はFindを、その位置を知りたい場合はFindIndexを使います。

C#
List<int> scores = [50, 70, 90, 40];

// 80以上の最初の要素を取得
int highSorted = scores.Find(s => s >= 80); // 90

// 80以上の最初の要素のインデックスを取得
int index = scores.FindIndex(s => s >= 80); // 2

もし条件に一致する要素が見つからない場合、Findは型のリテラル既定値(intなら0、参照型ならnull)を返します。

Listのソート(並べ替え)と反転

Listには標準で並べ替え機能が備わっています。

SortとReverse

C#
List<int> data = [5, 2, 8, 1, 9];

// 昇順にソート
data.Sort(); 
// 結果: [1, 2, 5, 8, 9]

// 要素の順序を反転
data.Reverse();
// 結果: [9, 8, 5, 2, 1]

独自のクラスや構造体をソートする場合は、IComparableインターフェースの実装や、デリゲートによる比較条件の指定が必要です。

LINQによる高度なList操作

C#の強力な機能であるLINQ(Language Integrated Query)を組み合わせることで、Listの操作は劇的に効率化されます。

LINQを使用するにはusing System.Linq;が必要です。

Whereによるフィルタリング

特定の条件に合致する要素だけを抽出します。

C#
using System.Linq;

List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 偶数だけを抽出して新しいリストを作成
List<int> evenNumbers = numbers.Where(n => n % 2 == 0).ToList();

Selectによる投影(変換)

各要素を別の形に変換します。

C#
List<string> words = ["apple", "banana", "cherry"];

// すべて大文字に変換したリストを作成
List<string> upperWords = words.Select(w => w.ToUpper()).ToList();

OrderByとThenBy

複数の条件で並べ替えを行う際に非常に便利です。

C#
var sortedList = words
    .OrderBy(w => w.Length)   // 文字数の長さで昇順
    .ThenBy(w => w)           // 同じ長さなら辞書順
    .ToList();

FirstOrDefaultとAny

  • FirstOrDefault: 条件に合う最初の要素を取得。なければ既定値を返す。
  • Any: 条件に合う要素が1つでもあるか判定。
C#
bool hasLargeNumber = numbers.Any(n => n > 100); // false

int firstMatch = numbers.FirstOrDefault(n => n > 5); // 6

LINQのメソッドの多くはIEnumerable<T>を返すため、最終的にListとして保持したい場合は最後に .ToList() を呼び出すことを忘れないようにしましょう。

Listのパフォーマンス最適化

大量のデータを扱う場合、Listの使い方次第でアプリケーションの速度やメモリ消費量が変わります。

Capacity(容量)の仕組みを理解する

Listは内部的に配列を持っており、要素数が容量を超えると、現在の2倍のサイズの新しい配列を確保し、全要素をコピーするという動作をします。

  1. 初期状態(Capacity = 0)
  2. 1つ追加(Capacity = 4程度 ※ランタイムによる)
  3. 5つ目追加(Capacity = 8へ拡張、コピー発生)
  4. 9つ目追加(Capacity = 16へ拡張、コピー発生)

この「拡張とコピー」が頻繁に発生するとパフォーマンスが低下します。

そのため、前述した通り初期化時に要素数を予測してnew List<int>(1000)のように指定することが推奨されます。

foreachとforの使い分け

Listの全要素にアクセスする場合、基本的にはforeachを使用します。

C#
foreach (var item in list)
{
    // 処理
}

現代のC#(特に.NET Core以降)では、foreachの最適化が進んでおり、配列やListに対する列挙は非常に高速です。

しかし、ループ内でインデックス(i)が必要な場合や、特定の条件下でパフォーマンスを極限まで追求する場合はfor文が選ばれることもあります。

構造体(Value Type)のList

List<int>などの値型のリストは、メモリ上で連続した領域に配置されるため、キャッシュ効率が良く非常に高速です。

一方で、巨大な構造体をListに入れると、コピーのコストが発生するため、設計には注意が必要です。

よくあるエラーと注意点

Listを使用する際に陥りやすい罠がいくつかあります。

ループ内での削除

foreachで回している最中に、そのリスト自体の要素をRemoveしようとすると、InvalidOperationExceptionが発生します。

C#
// 誤った例
foreach (var item in list)
{
    if (item.IsInvalid) list.Remove(item); // 例外発生!
}

この場合は、RemoveAllを使用するか、逆順のfor文で回すのが定石です。

C#
// 正しい例(逆順for)
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i].IsInvalid) list.RemoveAt(i);
}

参照型のListとディープコピー

Listの中にクラス(参照型)を入れている場合、リストを別の変数に代入しても、中身のインスタンスは共有されます。

C#
List<MyClass> list1 = [new MyClass { Id = 1 }];
List<MyClass> list2 = new List<MyClass>(list1);

list2[0].Id = 99;
Console.WriteLine(list1[0].Id); // 99 と表示される(共有されているため)

リストそのものだけでなく、中身のオブジェクトも複製したい場合は、ディープコピーの処理を自前で実装する必要があります。

読み取り専用のList

外部にリストを公開する際、勝手に要素を追加・削除されたくない場合があります。

そのときはAsReadOnlyメソッドやIReadOnlyList<T>を使用します。

C#
List<string> privateList = ["A", "B", "C"];

// 読み取り専用のビューを公開
IReadOnlyList<string> publicList = privateList.AsReadOnly();

// publicList.Add("D"); // コンパイルエラーになるため安全

これにより、カプセル化を維持しつつ、安全にデータを共有することが可能になります。

まとめ

C#のList<T>は、動的なサイズ変更、豊富な操作メソッド、そしてLINQとの親和性により、あらゆる開発シーンで中心的な役割を果たします。

  • 基本: AddRemoveで直感的に操作可能。
  • 最新: C# 12のコレクション式 [] で簡潔に記述。
  • 応用: LINQを活用して複雑なデータ抽出を1行で実現。
  • 最適化: 性能が必要な場面ではCapacityを意識する。

これらの特性を理解し、適切に使い分けることで、より堅牢で効率的なC#プログラムを記述できるようになります。

まずは基本的な操作をマスターし、徐々にLINQを用いた高度なデータ処理に挑戦してみてください。