C#のList<T>の使い方

配列に動的に要素を追加・削除する場合C#では、List<T> を使用します。今回はこのリストクラスの基本的な使い方を紹介したいと思います。紛らわしい名前でArrayList がありますが非推奨なので

使い方

名前空間の宣言

まず、使用する際は以下の名前空間を宣言します。

// 必須
using System.Collections.Generic;
// こっちは宣言しておくと便利な機能が使えるようになる
using System.Linq;

宣言と初期化

Listの宣言と初期値を指定して宣言する方法は以下の通りです。

List の T にリストで扱いたい型を指定する事で、指定型専用の入れ物として機能します。この指定方法は C# のジェネリックという機能です。以下例は int, string, 自作の型のリストを作成しています。

// int型を格納するリストを宣言、<int>の任意の型が指定可能
var list = new List<int>();

// 初期化した時に最初から値を指定する場合以下のように書く
var list2 = new List<int>()
{
    1, 2, 3, 4 // listには最初から1,2,3,4の4つの要素が含まれている
};

// Tにstring型を指定するとstring型を格納するリストになる
var stringList = new List<string>()
{
    "ika", "tako", "hotate" // 3つの文字列で宣言と同時に初期化する
};

// 自分で作成したオブジェクトも指定できる
var personList = new List<Person>()
{
    new Person() { Name = "ika" }, // 自作の型で宣言と同時に初期化
    new Person() { Name = "tako" },
    new Person() { Name = "hotate" },
};

// 配列をコンストラクタの引数に指定するとリストの初期値として設定される
var list3 = new List<int>(new [] { 0, 1, 2 });
// リストも同じように指定できる
var lisr4 = new List<int>(list3);

追加・挿入

各操作の概要は以下の通りです。

  • Add, 要素を末尾に追加
  • AddRange, 複数の要素を一気に追加
  • Insert, 指定した位置に要素を追加

それぞれの使い方は以下の通りです。

// 要素の追加
list.add(5); // 末尾に5を追加
list.add(6); // 末尾に6を追加
personList.Add(new Person() { Name = "kaki" }) // 自作のオブジェクトを末尾に追加

// 要素の挿入
list.Insert(0, 100); // 先頭(0番目)に100を挿入
list.Insert(3, 120); // 3番目に120を挿入
personList.Insert(2, new Person() { Name = "saba" }) // 自作のオブジェクトを2番目に挿入

// 他のリストを一気に追加
var list_a = new List<int>() { 1, 2, 3 };
var list_b = new List<int>() { 9, 8, 7 };
list_a.AddRange(list_b); // 1, 2, 3, 9, 8, 7

【補足】Clearメソッドを呼び出すと中身は全部消えますが、リスト自体の大きさ(=キャパシティ)は大きいままになります。1万件追加した後にクリアすると1万件分容量のある空のリストが存在することになります。これは TrimExcess() メソッドを呼せば小さくすることができます。

削除・クリア

各操作の概要は以下の通りです。

  • Remove, 要素を削除
  • RemoveAt, 指定した位置の要素を削除
  • Clear, リストの内容を全部削除

それぞれの使い方は以下の通りです。

// 要素の削除:1件削除
list.Remove(5);   // 最初に見つかった5を削除
list.RemoveAt(2); // 2番目の要素を削除

// 要素の削除:複数件削除
list.RemoveRange(0, 3);      // 0番目の要素から3件削除
list.RemoveAll(p => p == 2); // ラムダ式で指定した条件に一致するものを全て削除

// 全部削除
list.Clear();

【補足】Clearメソッドを呼び出すと中身は全部消えますが、リスト自体の大きさ(=キャパシティ)は大きいままになります。1万件追加した後にクリアすると1万件分容量のある空のリストが存在することになります。これは TrimExcess() メソッドを呼せば小さくすることができます。

値の取り出し、要素数、要素へのアクセス

  • [], 指定した位置の値の参照
  • Count, Listに含まれる要素数
  • for, 繰り返しで使用する
  • foreach, 繰り返しで使用する

Listのアクセス方法と要素の取り出し方は以下イメージです。

  • リストは配列のように添え字でアクセスできる
  • foreachで全部列挙することができる

それぞれの使い方は以下の通りです。

// 配列のように添え字で内容にアクセスできる
// 値が取り出せる
int item = list[2];

// リストの内容の個数を取得する
int count = list.Count(); // 要素が3つであれば3になる

// for文で先頭から順番にアクセスする
for(int i = 0; i < list.Count; i++)
{
    var item = list[i];
}

// foreachで最初から全部列挙する
foreach(int num in list)
{
    Console.WriteLine(num);
}

// foreachで最後から先頭へ逆順に全部列挙する
foreach(int num in list.Reverse())
{
    Console.WriteLine(num); 
}

ソート・逆順・抜き出し

各操作の概要は以下の通りです。

  • Sort, 内容を整列する
  • Reverse, 内容を逆順にする
  • Where, 条件を指定して抜き出す

リストの内容をソートするにはSort()メソッドを使います。整列は「デフォルトの整列規則」(=文字コード順)で整列されます。「デフォルトの整列規則」以外を使いたい場合、Sortメソッドに整列条件がラムダ式で渡せます。

// 辞書順にソート
var list = new List<int>() { 5, 2, 6, 3 };
list.Sort();
 // → 2, 3, 5, 6

// 自分のルールでソートする(逆順にソート
list.Sort((a, b) => a - b); // ラムダ式で自作の条件を記述できる
// → 6, 5, 3, 2

// 内容を逆順にできる
list.Reverse();

// 5以上のものを抜き出す
// ** 但し戻り値がListでは無いので注意
IEnumerable<int> result = list.Where(i => i > 5);
// こうすることで List に変換できる
List<int> result2 = result.ToList();

存在確認・検索

指定した要素がリスト内に存在するかを確認する方法と、その値を取得する方法です。

// 指定した値があるかどうか確認できる
if(list.Contains(3))
{
    // 存在する場合の処理
}

// ラムダ式で条件を指定して一致するものがあるか確認する
if(list.Exists(p => p == 3/*3に一致するものがある?*/))
{
    // 存在する場合の処理
}

// 指定した値が最初に見つかった要素のインデックスを取得できる
int index = list.IndexOf(1/*1がリストの中に存在するか問い合わせ*/);
if(index != -1) // 戻り値のindexが-1だと存在しない
{
    int item = list[index];
}

// ラムダ式で条件を指定し一致する最初の要素のインデックスを取得できる
int index = list.FindIndex(p => p == 1);
if (index != -1) // こっちも戻り値のindexが-1だと存在しない
{
    int item = list[index];
}

// 補足:
// Findメソッドは(未記載だが)指定条件がが見つからない場合<T>の既定値(intの場合0)を応答する
// なので<int>等の組み込み型の場合、ヒットしない場合の見分けがつかない

内容を検索する場合、リストの先頭から検索を中身で行います。存在しない場合最後まで検索しきってから答えが出るのでリストの件数が多い場合、割と時間がかかります。つまりO(N)の計算量となるため、度々検索すると動作速度が問題になる可能性があります。

別の型に変換, リスト同士の比較

  • ConvertAll, 条件を指定して別のリストに変換する
  • SequenceEqual, 2つのリストを先頭から比較する
var list = new List<int>() { 0, 1, 2, 3 };

// 別の型に変換する
List<double> list2 = list.ConvertAll(item => (double)item); // 変換条件をラムダ式で指定する

// ほかのリストと先頭からひとつづつ比較する
var list3 = new List<int>() { 0, 1, 2, 4 };
bool result = list.SequenceEqual(list3);
// result = false

配列に変換する

各操作の概要は以下の通りです。

  • ToArray, リストを配列に変換する
  • ToList, 別の方のリストに変換する(任意の IEnumerabe に変換する)
var list = new List<int>() { 0, 1, 2 };
// 配列に変換できる
int[] array = list.ToArray();
// 配列をリストに変換できる
var list2 = array.ToList();

List が継承するインターフェースの説明

Listクラスは、IEnumerable、ICollection, IListという複数のインターフェースを継承しています。メソッドの引数や戻り値で使用するときは用途に合わせてインターフェースで渡した方が基本的に拡張性が向上します。例えば、IList で引数の型を宣言すると呼び出し側は IList を継承しているクラス、List, SortedList, LinkedList を渡すことができるため、同じメソッドに渡すことができるので色々な型を広く受け取ることができます。

public ICollection<int> GetList() { // インターフェースで受け渡す

使い方によって、どのインターフェースを使えばいいかはざっくりと以下の通り。

interface 状況
IEnumerable(T) foreachで使える。シーケンスは途中で変更できない、イテレータで遅延評価、無限長の場合がある
ICollection(T) 追加、削除あり。インデクサでアクセスできないので純粋なリスト構造を表す
IList(T) 追加、削除あり。インデクサでアクセスできる。配列みたいに使える。有限の長さ

冒頭に触れた通り、 が付いていないほうの IList, ICollection, IEnumerable は基本使いません。

プロパティの場合、set 操作は private にしておいた方が安全です。ReadOnlyCollection(T) などに外から変更されて、クラス内で操作して例外が出るなどは想定外かと思います。

public ICollection<int> List { get; private set; } = new List<int>(); // setはprivateにしておく

アンチパターン

結構やりがちですが、以下のようなコードは基本的にお勧めできません。

var list = new List<List<int>>(); // <T>の中にジェネリックを宣言しない。

所謂多重リスト形式です。入れ子構造はいくつでも重ねることができますが、あとから見た時に理解しづらい、内容へのアクセスを制御できない、インスタンスの生成が非常にめんどくさい、などなど結構有害性があります。従ってこのような構造にならないように気を付けるかクラスでラッピングしたほうが良いです。

Listの類似クラス

List には以下のように似たようなリスト機能を持つクラスがいくつかあります。

クラス名 説明
System.Collections.ObjectModel.ReadOnlyCollection 読み取り専用のリスト。Listをラップする形で使用する。変更するとNotSupportedExceptionが発生する
System.Collections.Generic.SynchronizedCollection スレッド セーフのList。複数のTaskで結果をマージする時などに使う
SynchronizedReadOnlyCollection.SynchronizedCollection 上記Listを読み取り専用にするときに使う。滅多に使わない

ReadOnlyCollection は割と使います。クラス外からは変更してほしくない場合、以下のよう書くと中からは変更可能で外からは変更不可能になります。

public class ReadOnly
{
    // 外部に公開する読み取り専用のプロパティ(Listをラップしている
    public ReadOnlyCollection<string> List => new ReadOnlyCollection<string>(this.sourceList);

    // 中で使用するリスト
    private readonly List<string> sourceList = new List<string>() { "123", "456", "789", "abc" };

    // 文字列を追加
    public void Foo(string str) => this.sourceList.Add(str);
}

使用方法は以下の通り。中からは操作できて外からは変更不可能なListの公開ができる。

static void Main(string[] args)
{
    var r = new ReadOnly();
    foreach (string item in r.List)
    {
        Console.Write(item + ", ");
        
    }
    // > 123, 456, 789, abc,

    r.Foo("xxxx");
    foreach (string item in r.List)
    {
        Console.Write(item + ", ");
    }
    // > 123, 456, 789, abc, xxxx,
    // 中からの変更は反映される

    //r.List[0] = 100;
    //// ↑
    //// これはできない
    //// > エラー CS0200 プロパティまたはインデクサー 'ReadOnlyCollection<string>.this[int]' 
    //// > は読み取り専用であるため、割り当てることはできません

    ((IList<string>)r.List).Add("asdf");
    // 無理やりキャストして実行すると例外になる
    // System.NotSupportedException: 'コレクションは読み取り専用です。'
}

以上です。