Listクラス

Listクラスとは

配列を使用すれば同じデータ型の変数をまとめて扱うことができます。
これは便利な機能ですが、C#ではListクラスという、配列をさらに便利にしたようなものが用意されています。


using System;
using System.Collections.Generic; //←これが必要

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            //配列
            int[] arrNum = new int[] { 1, 2, 3, 4, 5 };

            //Listクラス
            List<int> lstNum = new List<int>() { 1, 2, 3, 4, 5 };

            //Listの要素数は「Count」で取得
            for(int i = 0; i < lstNum.Count; i++)
            {
                //各要素へのアクセスは配列と同じ
                Console.WriteLine(lstNum[i]);
            }

            Console.ReadLine();
        }
    }
}
1
2
3
4
5

Listクラスを使用するには、コードの先頭に「using System.Collections.Generic;」と記述する必要があります。
Visual Studioでプロジェクトを作成すれば自動で追加されていると思いますが、念のため確認してください。
(その他の「using ~」の行は変更しなくて良いです)

さて、14行目でListクラスの使用を宣言しています。
Listクラスは以下の形式で使用します。


List<データ型> 変数名 = new List<データ型>();

List<データ型> 変数名 = new List<データ型>() { 初期化子 };

List<データ型> 変数名 = new List<データ型> { 初期化子 };

「List」に続く「<>」の中に、扱いたいデータ型を指定します。
次に変数名を書き、続いて「= new List<データ型>」と記述します。
最後に丸括弧()を記述します。
これを忘れるとエラーになるので注意してください。

Listクラスの使用宣言はここまでですが、配列の時と同じように続いて初期化子(初期化リスト)を与えることができます。
書き方も配列と同じなので難しいことはないでしょう。
初期化子を使用する場合は丸括弧は省略できます。

配列は初期化時に要素数を数値で指定できましたが、Listにはそういった機能はなく、初期化子を使用しなければ初期の要素数はゼロとなります。
(コンストラクターを使用するという方法もあります)

こうして宣言&初期化されたListは、配列とほぼ同じように扱うことができます。
各要素へのアクセスは添字演算子[]で行います。

Listを含め、クラス型の変数のことはインスタンスと呼ばれることが多いです。

要素数

配列はLengthプロパティで要素数を取得しますが、ListはCountプロパティで要素を取得します。


List<int> lstNum = new List<int>() { 1, 2, 3, 4, 5 };
int count = lstNum.Count; //5

ジェネリックコレクション

コード先頭の「using System.Collections.Generic」にあるように、この機能はコレクションの機能のうちのジェネリックコレクションという機能に分類されます。

コレクションというのはデータの集合を意味します。
配列もコレクションの一種ですが、これはC#のごく基本的な機能なのでここでは省いて考えます。

ジェネリックに関しては詳しくはジェネリックの項で説明しますが、簡単に言えば任意のデータ型を引数に取れる機能です。
「任意のデータ型の変数」ではなく「任意のデータ型」を引数に取れます。
例えばListクラスは、int型、string型、その他ユーザー定義型などあらゆるデータ型を型引数に指定することで、任意のデータ型の集合を扱うことができます。

Listクラスはコレクションとジェネリックの両方の機能を持つのでジェネリックコレクションと呼ばれます。
Listクラス以外にもジェネリックコレクションは多数用意されており、それぞれに特徴があります。

ジェネリックコレクションは非常に便利で、C#にジェネリックコレクション機能が追加されてからはジェネリックでないコレクションはあまり使用されなくなっています。
当サイトでは非ジェネリックコレクションは基本的に解説しません。
(非ジェネリックコレクションは「「using System.Collections」内に存在します」)

型推論が便利

上記の記述方法は「List<int>」を二回も書かないといけないので、少し冗長です。
このような場合は型推論を用いるとスッキリします。


var lstNum = new List<int>() { 1, 2, 3, 4, 5 };

Listクラスのメリット

Listクラスと配列との最大の違いは、動的に要素を追加、削除できるという点です。


List<int> lst = new List<int>() { 0, 1, 2, 3 };

//「9」を追加
lst.Add(9);

foreach (var n in lst)
    Console.Write("{0}, ", n);

Console.WriteLine();

//「2」を削除
lst.Remove(2);

foreach (var n in lst)
    Console.Write("{0}, ", n);

Console.ReadLine();
0, 1, 2, 3, 9,
0, 1, 3, 9,

これと同じことを配列で実現しようとするとなかなか面倒な手順を踏む必要がありますが、Listクラスならばメソッド一つで簡単に実現でき、なおかつ高速に動作します。

あらかじめ要素数が決まっていて増減することがない場合はListクラスよりも配列を使用したほうが高速に動作する可能性があります。
とはいえ体感できるような速度差が出ることはほぼないので、配列の代わりにListクラスを使用しても問題ありません。

Listクラスのメソッドの具体的な操作方法は次ページ(Listクラスのメソッド)で解説します。

Listのコピー

Listクラスは配列と同じく参照型です。
(値型と参照型を参照)
つまり、List型変数に別のList型変数を代入しただけではコピーにはなりません。


List<int> lst1 = new List<int>() { 1, 2, 3 };

//「lst2」は「lst1」の別名になるだけ
List<int> lst2 = lst1;

Listクラスをコピーするには、List宣言時の丸括弧内にListクラスの変数を渡すという方法があります。
これはコンストラクターによる初期化、といいます。


List<int> lst1 = new List<int>() { 1, 2, 3 };

//lst1の複製を作る
List<int> lst2 = new List<int>(lst1);

//コピー元を書き換えてみる
lst1[1] = 9;

foreach (var n in lst1)
    Console.Write("{0}, ", n);

Console.WriteLine();

foreach (var n in lst2)
    Console.Write("{0}, ", n);
1, 9, 3,
1, 2, 3,

その他、ToListメソッドを呼び出すことでもコピーできます。
ToListメソッドの使用にはコード先頭に「using System.Linq;」が必要です。


using System;
using System.Collections.Generic;
using System.Linq; //←これが必要

namespace ConsoleApplication1
{
//以降省略

List<int> lst1 = new List<int>() { 1, 2, 3 };

//lst1の複製を作る
List<int> lst2 = lst1.ToList();

//以下は結果は最初のコードと同じなので省略

ただし、これらの方法によるコピーはシャローコピーです。
参照型を要素とするListの複製はアドレスのコピーに過ぎないので注意してください。
詳しくはシャローコピーとディープコピーを参照してください。

要素の削除時の注意点

Listの要素は任意に削除することもできますが、いくつかの注意点があります。
なお、以下のサンプルコードで使用しているRemoveAtメソッドは、引数に指定した番号の要素を削除するメソッドです。
詳細は次ページのRemoveAtで解説します。

削除で要素がずれることを考慮する

Listの要素を削除すると、削除した要素の後ろの要素がひとつ前に詰められます。
削除メソッドを単体で使用する場合は問題ありませんが、ループ文で削除メソッドを使用する場合は注意が必要です。

例えば、string型Listの要素から文字数が6未満の要素をすべて削除するつもりで以下のようなコードを書いたとします。


static void Main(string[] args)
{
    var lst = new List<string>()
        { "Apple", "Grape", "Orange", "Strawberry", "Peach" };

    //Listから文字数が6未満の要素を削除するつもり
    for (int i = 0; i < lst.Count; i++)
    {
        if (lst[i].Length < 6)
            lst.RemoveAt(i);
    }

    for (int i = 0; i < lst.Count; i++)
    {
        Console.WriteLine(lst[i]);
    }
}
Grape
Orange
Strawberry

「Grape」は5文字なのに削除されずに残ってしまっています。

これは、先頭の「Apple」が削除された際に、後ろの要素が前に詰められているために起こります。
つまり「Apple」が削除されるとそれ以降の要素が前にずれるので、「Grape」の要素番号は「0」になっています。
にもかかわらず、ループカウンタ(変数i)は先に進んでしまうので、「Grape」の文字数判定が行われることなく処理が進んでしまっているのです。

これを正しく動作するように書き直すと以下のようになります。


static void Main(string[] args)
{
    var lst = new List<string>()
        { "Apple", "Grape", "Orange", "Strawberry", "Peach" };

    //Listから文字数が6未満の要素を削除する
    for (int i = lst.Count - 1; i >= 0; i--)
    {
        if (lst[i].Length < 6)
            lst.RemoveAt(i);
    }

    for (int i = 0; i < lst.Count; i++)
    {
        Console.WriteLine(lst[i]);
    }
}
Orange
Strawberry

ループを要素の先頭から行うのではなく、末尾から先頭に向けて行うようにします。
こうすることで、要素が削除されて前に詰められても影響なく削除処理ができます。
「ループで削除処理を行う場合は末尾から」ということは覚えておきましょう。

foreachで削除しない

foreach文では削除系のメソッドは使用できません。
使用するとエラー(例外)が発生します。
要素の削除ができないというよりも変更が禁止されているので、追加系のメソッドも使用できません。