C#における開発において、動的配列を扱うList<T>クラスは最も頻繁に使用されるコレクションの一つです。

データの追加や削除が容易である反面、初期化の方法や初期容量の設定次第では、アプリケーションのパフォーマンスに影響を与える可能性もあります。

近年ではC# 12で導入された「コレクション式」により、さらに簡潔で直感的な記述が可能になりました。

本記事では、基本的な初期化方法から、最新の構文、さらにはパフォーマンスを最適化するためのテクニックまでを網羅的に解説します。

List<T>の基本的な初期化方法

C#でリストを扱う際、最も一般的で古くから使われているのがnewキーワードを用いたコンストラクタの呼び出しです。

Listはジェネリッククラスであるため、格納する要素の型を指定してインスタンス化します。

空のリストを作成する

最もシンプルな初期化は、要素を持たない空のリストを作成する方法です。

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

class Program
{
    static void Main()
    {
        // 最も標準的な空のリストの初期化
        List<string> fruits = new List<string>();

        // 要素を追加
        fruits.Add("Apple");
        fruits.Add("Banana");

        Console.WriteLine($"要素数: {fruits.Count}");
        foreach (var fruit in fruits)
        {
            Console.WriteLine(fruit);
        }
    }
}
実行結果
要素数: 2
Apple
Banana

この方法では、インスタンス化した時点では要素がゼロの状態です。

動的に要素を追加していく場合に適していますが、内部的なメモリ確保の仕組みについては後述する「初期容量」のセクションで詳しく解説します。

ターゲット型のnew式 (C# 9.0以降)

C# 9.0からは、変数の型宣言から型が推論できる場合に、newの後の型名を省略できる「ターゲット型のnew式」が利用可能になりました。

C#
// 従来の書き方
List<int> numbers1 = new List<int>();

// C# 9.0以降の簡略化された書き方
List<int> numbers2 = new();

この記述法は、特にフィールド変数やプロパティの初期化時にコードをスッキリさせる効果があります。

冗長な型名を繰り返す必要がないため、可読性が向上します。

コレクション初期化子による初期化

リストの作成と同時に、いくつかの要素をあらかじめ登録しておきたい場合には「コレクション初期化子」を使用します。

これはC# 3.0から導入された機能で、波括弧 {} を用いて記述します。

基本的な書き方

C#
List<string> colors = new List<string>
{
    "Red",
    "Blue",
    "Green"
};

この構文を使用すると、コンパイラが自動的に内部で Add メソッドを呼び出すコードを生成します。

そのため、カスタムクラスでこの構文を使いたい場合は、そのクラスが IEnumerable を実装し、かつ適切な Add メソッドを持っている必要があります。

ターゲット型のnew式との組み合わせ

前述のターゲット型のnew式と組み合わせることで、さらに短く記述できます。

C#
List<int> scores = new() { 80, 90, 75 };

最新機能:コレクション式 (C# 12以降)

C# 12では、コレクションの初期化における決定版とも言える「コレクション式 (Collection Expressions)」が導入されました。

これにより、配列やList、Spanなどをすべて同じ角括弧 [] の構文で初期化できるようになりました。

コレクション式の基本

C#
// C# 12以降の推奨される書き方
List<string> names = ["Alice", "Bob", "Charlie"];

// 空のリストも簡潔に表現可能
List<int> emptyList = [];

この構文の最大のメリットは、左辺の型が何であれ、右辺を共通の書き方に統一できる点にあります。

例えば、後から型を List<T> から T[]Span<T> に変更したとしても、右辺の初期化コードを書き換える必要がありません。

スプレッド演算子による展開

コレクション式の中では、スプレッド演算子 .. を使用して、既存のコレクションの要素を別のリストに展開して取り込むことができます。

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

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

        // 既存のリストを組み合わせて新しいリストを作成
        List<int> allNumbers = [0, ..firstHalf, ..secondHalf, 7];

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

これまで複数のリストを結合するには AddRange メソッドや LINQ の Concat を使用する必要がありましたが、コレクション式を使えば宣言的かつ直感的に記述できます。

パフォーマンスを意識した初期化:Capacityの指定

List<T>の内部構造は「動的配列」です。

要素が増えて内部配列のサイズが足りなくなると、より大きなメモリ領域を確保し直して全要素をコピーするという処理が発生します。

これを「リサイズ(再確保)」と呼びます。

大量のデータを扱う場合、このリサイズ処理がパフォーマンスのボトルネックになることがあります。

初期容量 (Capacity) の指定方法

あらかじめ格納する要素の数がわかっている場合は、コンストラクタで初期容量を指定するのがベストプラクティスです。

C#
// 10,000個の要素が入る領域を最初から確保する
List<int> largeList = new List<int>(10000);
プロパティ名意味
Count現在リストに格納されている実際の要素数
Capacity再確保なしに格納できる要素の総数(メモリ確保サイズ)

以下のサンプルコードで、リサイズが発生する様子を確認してみましょう。

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

class Program
{
    static void Main()
    {
        List<int> list = new List<int>();
        Console.WriteLine($"初期状態: Count={list.Count}, Capacity={list.Capacity}");

        for (int i = 1; i <= 10; i++)
        {
            list.Add(i);
            Console.WriteLine($"{i}個追加後: Count={list.Count}, Capacity={list.Capacity}");
        }
    }
}

実行結果(環境により数値は異なる場合があります):

初期状態: Count=0, Capacity=0
1個追加後: Count=1, Capacity=4
2個追加後: Count=2, Capacity=4
3個追加後: Count=3, Capacity=4
4個追加後: Count=4, Capacity=4
5個追加後: Count=5, Capacity=8
6個追加後: Count=6, Capacity=8
7個追加後: Count=7, Capacity=8
8個追加後: Count=8, Capacity=8
9個追加後: Count=9, Capacity=16
10個追加後: Count=10, Capacity=16

このように、要素が不足するたびに Capacity が段階的に拡張(一般的には2倍ずつ)されます。

初期容量を適切に設定することで、メモリの再確保とデータのコピーを回避し、実行速度を向上させることができます。

既存のデータからの初期化

既存の配列や他のコレクション(IEnumerableを実装したもの)からListを作成することも頻繁にあります。

配列やIEnumerableを渡す

コンストラクタにコレクションを渡すことで、その要素をコピーして初期化できます。

C#
string[] array = { "C#", "VB.NET", "F#" };

// 配列を元にリストを作成
List<string> languages = new List<string>(array);

LINQのToList()メソッドを使用する

LINQを使用してデータを加工し、その結果をリストとして保持したい場合は ToList() メソッドを使用します。

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

class Program
{
    static void Main()
    {
        IEnumerable<int> query = Enumerable.Range(1, 10).Where(n => n % 2 == 0);

        // LINQの結果をリスト化
        List<int> evenNumbers = query.ToList();

        Console.WriteLine(string.Join(", ", evenNumbers));
    }
}
実行結果
2, 4, 6, 8, 10

ただし、ToList() は呼び出されるたびに新しいリストのインスタンスを生成するため、ループ内での過剰な呼び出しには注意が必要です。

特殊な初期化と読み取り専用リスト

用途によっては、値を変更できないリスト(イミュータブルなリスト)や、読み取り専用のビューが必要になることがあります。

ReadOnlyCollectionによる初期化

既存のListをラップして、外部から変更できないようにするには AsReadOnly メソッドを使用します。

C#
List<string> editableList = ["Item1", "Item2"];
IList<string> readOnlyView = editableList.AsReadOnly();

// readOnlyView.Add("Item3"); // コンパイルエラーまたは実行時例外

ImmutableList (System.Collections.Immutable)

より厳格な不変性が必要な場合は、ImmutableList を使用します。

これを利用するには、NuGetパッケージの追加が必要な場合があります。

C#
using System.Collections.Immutable;

ImmutableList<int> immutable = [1, 2, 3];
// 変更を加える場合は新しいインスタンスが返される
ImmutableList<int> nextImmutable = immutable.Add(4);

Listの初期化におけるベストプラクティス

状況に応じて最適な初期化方法を選択するための指針をまとめます。

1. 要素数が固定または既知の場合

C# 12以降であればコレクション式 [] を使用してください。

これが最も簡潔で、今後の標準となる書き方です。

2. 空のリストを作成し、後から大量に追加する場合

パフォーマンスを重視し、コンストラクタで初期容量(Capacity)を指定してください。

リサイズ回数を減らすことは、GC(ガベージコレクション)の負荷軽減にもつながります。

3. 他のコレクションから変換する場合

要素を加工する必要がなければ、コンストラクタに直接渡すのが効率的です。

LINQの操作が必要な場合のみ ToList() を使用しましょう。

4. クラスのプロパティを初期化する場合

C# 9.0以降であればターゲット型のnew式 new() が便利ですが、C# 12以降で初期値もセットするならコレクション式 [] を推奨します。

まとめ

C#のList初期化は、言語の進化とともに多様化してきました。

  • 基本的な初期化: new List<T>() やターゲット型の new()
  • 値を持たせた初期化: C# 12の**コレクション式 []** が現在最も推奨される。
  • 展開: スプレッド演算子 .. で既存のコレクションをマージできる。
  • 最適化: 要素数が多い場合はCapacityを指定して無駄なリサイズを防ぐ。
  • 変換: ToList() やコンストラクタ経由で他の型から生成。

開発しているプロジェクトのC#バージョンを確認し、その環境で利用可能な最もクリーンで効率的な初期化方法を選択してください。

特に最新のコレクション式は、記述量を減らすだけでなく、コードの意図を明確にする強力なツールです。

これらをマスターすることで、よりモダンでパフォーマンスの高いC#プログラミングが可能になります。