なぜ、List<T>の理解が重要なのか、その理由

この記事では、当社の新人エンジニア研修の参考にC#を解説します。

前回は「例外処理で想定外の事態に強いシステムにする」について学びました。

今回は、List<T>について解説します。また、コレクションジェネリクスについても触れていきます。

大量のデータを扱う際には配列が便利でした。しかし、配列はサイズが固定されていました。そのため、データ数が事前に決まっていない場面では扱いづらく、要素を動的に増減したい場合には不便でした。この問題を解決するのがListです。Listはサイズを自由に変更でき、型安全性も保ちつつ柔軟なデータ管理を実現します。

コレクションとジェネリクスは、Listを理解する上で欠かせない概念です。コレクションとは複数の要素をまとめて管理する仕組みで、Listもその一つです。また、<T>のような表記をジェネリクスといいます。ジェネリクスを利用することで、要素の型をあらかじめ指定できるため、型安全で効率的な処理を実現します。型変換時のミスを防ぎ、コードの再利用性も高まるのです。

1. List<T> とは

C#には、配列と同様に「オブジェクトや値をまとめる」ためのデータ構造が多数あります。中でも定番なのがList<T>。これは「要素数を自動的に伸縮できる配列のようなもの」であり、便利なメソッドを持っています。

新人エンジニア研修
List<T>のイメージ

T型パラメータを表します(これを「ジェネリクス」というのでした)。

例: 文字列リスト

using System;
using System.Collections.Generic;

public class Example01
{
    public static void Main()
    {
        List<string> list = new List<string>();
        list.Add("abc");
        list.Add("def");
        list.Add("ghi");

        Console.WriteLine(list[0]);  // "abc"
    }
}

<実行結果>

abc

なぜ型パラメータが必要?

同じList<...>でも、格納する要素の型がどんな型かを指定しないとコンパイラが判断できません。誤った型を混在させることを防ぎ、型安全を確保するため、<T> で要素型を宣言します。

2. ジェネリクス(Generics)

C#では「ジェネリクス」という機能で「再利用可能な型引数つきのクラス・メソッド」を設計できます。
List<T>はその代表例で、Tには参照型でも値型(intなど)でも指定可能です。

例: 整数型リスト

using System;
using System.Collections.Generic;

public class Example02
{
    public static void Main()
    {
        List<int> list = new List<int>();
        list.Add(1);
        list.Add(2);
        list.Add(3);

        // 合計を求める
        Console.WriteLine(list[0] + list[1] + list[2]); // 6
    }
}

3. LinkedList<T>との使い分け

新人エンジニア研修
List<T>LinkedList<T>の関係

C#には LinkedList<T> という連結リスト構造もあります。
LinkedList<T> はリスト先頭への要素の追加・削除が高速ですが、ランダムアクセスが遅いなど特徴があります。

List<T> は内部実装が配列の動的拡張であり、「先頭要素を削除」すると配列コピーが入るので遅め、逆に末尾への追加は高速、ランダムアクセスも高速です。

using System;
using System.Collections.Generic;
using System.Diagnostics;

public class Example03
{
    public static void Main()
    {
        const int N = 10000;
        
        // List<T>に1万件追加
        var list1 = new List<int>();
        for (int i = 0; i < N; i++)
        {
            list1.Add(i);
        }

        var stopwatch = Stopwatch.StartNew();
        // 先頭要素を連続削除
        for (int i = 0; i < N/2; i++)
        {
            list1.RemoveAt(0);
        }
        stopwatch.Stop();
        Console.WriteLine($"List 先頭削除 {N/2}回: {stopwatch.ElapsedMilliseconds} ms");

        // LinkedList<T>に1万件追加
        var list2 = new LinkedList<int>(list1);

        stopwatch.Restart();
        // 先頭要素を連続削除
        for (int i = 0; i < list2.Count/2; i++)
        {
            list2.RemoveFirst();
        }
        stopwatch.Stop();
        Console.WriteLine($"LinkedList 先頭削除 {N/2}回: {stopwatch.ElapsedMilliseconds} ms");
    }
}

実行すると、List<T>の先頭削除が遅い、LinkedList<T>は先頭削除は高速といった結果になる傾向があると思います。(環境によります)

新人エンジニア研修
List<T>LinkedList<T>の違い
  1. List<T>: ランダムアクセス高速 / 末尾追加高速 / 先頭削除・挿入遅め
  2. LinkedList<T>: 先頭・中間の削除・挿入が部分的に高速 / ランダムアクセス苦手

4. IList<T>ICollection<T>

C#のList<T>は「動的配列」の具象クラスですが、インターフェースとして IList<T>ICollection<T> を使う場合があります。これは「柔軟に差し替えられるようにしたい」ときなどに便利です。

新人エンジニア研修
C#のコレクション系クラスとインターフェース
IList<string> list = new List<string>();
// あとで new LinkedList<string>() などに付け替えやすい

Removeメソッドを安全に使えるかどうかが異なります。
一般的には「コレクションをforeach中に変更しない」が原則で、変更したいなら別の方法(例えば forループ、または .Where(...) で抽出)を推奨します。

  1. C#のジェネリクスT を指定することで、型安全かつパフォーマンス良くデータを扱える
  2. LinkedList<T>など他のコレクションもあり、用途に応じて使い分ける
  3. インターフェース (IList<T> など) で変数を宣言すれば差し替え可能
  4. foreach中の要素削除は注意(InvalidOperationException)

これで、コレクションフレームワークの基礎が学べました。
C#でもList<T>を中心に覚えれば「要素数を動的に管理する」作りができ、ロジックを組む上で強力なツールとなります。追加でDictionary<TKey,TValue>, HashSet<T>など他のコレクションも学んでみるとさらに便利さが分かるでしょう。


<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ List<T>は配列のように要素数を自由に伸縮できるListは、要素数に応じて動的に伸縮する配列である。要素の追加、削除、検索など、便利なメソッドを多く持つ。

□ ジェネリクスとは、クラスやメソッドに型パラメータを与えることで、様々な型に対応する汎用的な設計を可能にする機能である。C#では<>記号を使用して型を指定する。

□ C#のコレクション系クラスとインターフェースとは、オブジェクトの集合を効率的に操作するために設計されたクラス群を指す。C#ではSystem.CollectionsとSystem.Collections.Generic名前空間に含まれるクラスが該当する。