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

配列とは異なり、動的にサイズを変更できるこのコレクションは、柔軟なデータ操作を可能にします。

しかし、その利便性を最大限に引き出すためには、要素を追加するための各種メソッドの特性を正しく理解し、状況に応じた最適な手法を選択することが不可欠です。

本記事では、基本的な Add メソッドから、一括処理の AddRange、特定位置への挿入を行う Insert、さらには最新の C# で導入された効率的な記法まで、パフォーマンス面を含めて徹底的に解説します。

C# の List における要素追加の基本

C# の List<T> は、内部的に配列を使用して要素を管理しています。

要素を追加する際、内部配列の容量 (Capacity) が不足すると、より大きな配列を確保して既存の要素をコピーするという処理が自動的に行われます。

この仕組みを理解しておくことは、効率的なコードを書くための第一歩となります。

Add メソッド:単一要素の追加

最も基本的かつ多用されるのが Add メソッドです。

このメソッドは、リストの末尾に新しい要素を一つ追加します。

Add メソッドの使い方

Add メソッドは、引数にリストの型 T と一致するインスタンスを渡すだけで使用できます。

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

class Program
{
    static void Main()
    {
        // 文字列型のリストを初期化
        List<string> fruits = new List<string>();

        // 要素を一つずつ追加
        fruits.Add("Apple");
        fruits.Add("Banana");
        fruits.Add("Orange");

        // 内容の表示
        foreach (var fruit in fruits)
        {
            Console.WriteLine(fruit);
        }
    }
}
実行結果
Apple
Banana
Orange

パフォーマンス上の特徴

Add メソッドの計算量は、基本的には O(1) です。

ただし、前述の通り内部配列の拡張が発生した場合には、全要素のコピーが発生するため一時的に O(n) の負荷がかかります。

大量のデータをループ内で追加する場合は、この拡張回数を減らす工夫が重要になります。

AddRange メソッド:複数要素の一括追加

複数の要素を一度に追加したい場合、ループ内で Add を繰り返すよりも AddRange メソッドを使用する方が賢明です。

AddRange のメリット

AddRange は、IEnumerable<T> インターフェースを実装したコレクション(配列、他のリスト、LINQの結果など)を引数に取ります。

最大のメリットは、内部配列の再確保回数を最小限に抑えられることです。

例えば、10個の要素を追加する場合、Add を10回呼ぶと途中で複数回のメモリ再確保が発生する可能性がありますが、AddRange は追加する数があらかじめ分かっている場合、一度の拡張で済ませることができます。

AddRange メソッドのコード例

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

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3 };
        
        // 追加するための配列
        int[] extraNumbers = { 4, 5, 6 };

        // 一括追加
        numbers.AddRange(extraNumbers);

        // リスト同士の結合も可能
        List<int> moreNumbers = new List<int> { 7, 8 };
        numbers.AddRange(moreNumbers);

        Console.WriteLine(string.Join(", ", numbers));
    }
}
実行結果
1, 2, 3, 4, 5, 6, 7, 8

パフォーマンスを意識するなら、複数の要素を扱うときは常に AddRange を検討してください。

Insert・InsertRange メソッド:位置を指定した挿入

リストの末尾ではなく、特定の位置(インデックス)に要素を割り込ませたい場合は、Insert または InsertRange を使用します。

Insert メソッドの使い方

第一引数に挿入したい位置のインデックス(0から始まる番号)、第二引数に要素を指定します。

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

class Program
{
    static void Main()
    {
        List<string> tasks = new List<string> { "Task1", "Task3" };

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

        // 先頭に挿入
        tasks.Insert(0, "Priority Task");

        foreach (var task in tasks)
        {
            Console.WriteLine(task);
        }
    }
}
実行結果
Priority Task
Task1
Task2
Task3

Insert / InsertRange の注意点と負荷

Insert メソッドを使用する際は、その計算量に注意が必要です。

List<T> の内部構造は配列であるため、特定の位置に要素を挿入すると、それ以降にあるすべての要素を後ろに一つずつずらす処理が発生します。

メソッド挿入位置計算量
Add末尾O(1) ※平均
Insert先頭O(n)
Insert中間O(n)

特にリストの要素数が多い場合、先頭付近への挿入を繰り返すと、プログラムの実行速度が著しく低下する原因となります。

頻繁に先頭への挿入が発生するシナリオでは、LinkedList<T> などの他のデータ構造を検討すべきです。

最新の C# における要素追加の記法(C# 12以降)

C# 12 では、「コレクション式 (Collection Expressions)」という新しい記法が導入されました。

これにより、リストの初期化や要素の追加がより直感的に記述できるようになりました。

スプレッド演算子による結合

スプレッド演算子 .. を使用すると、既存のコレクションを新しいリストの中に展開して含めることができます。

これは AddRange と似た役割を果たしますが、より宣言的に記述できます。

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

class Program
{
    static void Main()
    {
        int[] array = [1, 2, 3];
        List<int> existingList = [4, 5, 6];

        // コレクション式による新しいリストの作成と結合
        List<int> combined = [0, ..array, ..existingList, 7];

        Console.WriteLine(string.Join(", ", combined));
    }
}
実行結果
0, 1, 2, 3, 4, 5, 6, 7

この記法はコードの可読性を高めるだけでなく、コンパイラによって最適化されるため、パフォーマンス面でも非常に優れています。

パフォーマンスを最適化する「Capacity」の管理

List<T> の要素追加で最もコストがかかるのは、内部配列のサイズ拡張と要素のコピーです。

デフォルトでは、リストに要素が追加されて容量が足りなくなると、容量は「現在の2倍」に拡張されます。

事前の容量指定 (Capacity Property)

追加する要素の数が事前にある程度予測できている場合は、コンストラクタで初期容量を指定するか、Capacity プロパティを明示的に設定することで、不必要なメモリ再確保を避けることができます。

C#
// 10,000個の要素を入れることが分かっている場合
List<int> largeList = new List<int>(10000); 

// または後から指定
largeList.Capacity = 20000;

このように 初期容量を指定することで、実行速度を劇的に改善できるケースが多くあります。

Count と Capacity の違い

混同されやすいですが、以下の違いを明確に理解しておきましょう。

  • Count:現在リストに格納されている実際の要素数(読み取り専用に近い)。
  • Capacity:メモリ上に確保されている内部配列のサイズ(読み書き可能)。

現場で役立つ実戦的なテクニック

null チェックと安全な追加

外部ソース(APIやデータベースなど)から取得したコレクションを AddRange する場合、そのソースが null であると例外が発生します。

C# の null 許容参照型や null 合体演算子を活用しましょう。

C#
List<string> myList = new List<string>();
List<string>? source = GetNullableList();

// source が null の場合は空のリストとして扱う
myList.AddRange(source ?? []);

LINQ を活用した条件付き追加

特定の条件に合致する要素だけを抽出して追加したい場合は、LINQ の Where メソッドと組み合わせるのが一般的です。

C#
using System.Linq;

List<int> numbers = new List<int> { 1, 10, 5, 20 };
List<int> filterdList = new List<int>();

// 10以上の数値だけを一括追加
filterdList.AddRange(numbers.Where(n => n >= 10));

要素追加時の注意点とアンチパターン

foreach ループ内での追加

foreach ループの中で、回しているリスト自身に対して Add や Insert を行うことは厳禁です。

これを行うと、実行時に InvalidOperationException がスローされます。

C#
// NGな例
foreach (var item in myList)
{
    if (item.Condition)
    {
        myList.Add(newItem); // ここで例外が発生
    }
}

この場合は、追加したい要素を一時的な別のリストに保存しておき、ループ終了後に AddRange するか、for 文を使用してインデックス管理を行う必要があります。

スレッドセーフティの欠如

List<T> は標準ではスレッドセーフではありません。

複数のスレッドから同時に要素を追加しようとすると、内部データが破損したり、予期せぬ例外が発生したりします。

マルチスレッド環境で要素を追加する場合は、lock 文を使用するか、System.Collections.Concurrent 名前空間にある ConcurrentBag<T> などの利用を検討してください。

まとめ

C# の List<T> における要素追加は、一見単純ですが、その裏側にある仕組みを理解することで、より堅牢で高速なアプリケーションを構築できるようになります。

  • 単一要素の追加には、直感的な Add を使用する。
  • 複数要素の一括追加には、パフォーマンスに優れた AddRange を優先する。
  • 特定位置への挿入には Insert を使用するが、要素数が多い場合のパフォーマンス低下に注意する。
  • 大量のデータを扱う際は、Capacity を事前に設定してメモリ再確保のコストを抑える。
  • モダンな C# では、コレクション式を活用して簡潔に記述する。

これらのメソッドを適切に使い分けることで、コードの可読性と実行効率を高いレベルで両立させることができます。

開発シーンに合わせて最適な選択ができるよう、本記事の内容をぜひ参考にしてください。