コレクション【C#】

C#文法:コレクション:C03_List_動的な要素の変更

今回は List<T> の「動的な要素の変更」を取り上げます。配列は生成時に要素数が決まってしまうため、あとから要素を増やしたり減らしたりするには Array.Resize や Array.Copy を組み合わせる必要がありました。List<T> であれば、追加・挿入・削除のためのメソッドが最初から用意されているので、メソッドを1つ呼び出すだけで要素数を自由に変えられます。

この章では、追加系の Add・AddRange、挿入系の Insert・InsertRange、削除系の Remove・RemoveAt・RemoveRange・RemoveAll・Clear の9つのメソッドを、1つずつコードを書き足しながらステップ実行し、ウォッチウィンドウで list の中身がどう変化するかを確認していきます。

テスト用のボタンを追加する

これまでと同じように、フォームに新しいボタンを1つ追加します。ボタンのテキストは「List<T> 動的な要素の変更」とし、ダブルクリックしてクリックイベント(button15_Click)を生成します。今回のコードはすべてこのメソッドの中に書いていきます。

図1 フォームに「List<T> 動的な要素の変更」ボタンを追加する

図1 フォームに「List<T> 動的な要素の変更」ボタンを追加する

Add:末尾に1件追加する

まずは string の List を生成します。要素の追加は前回も見たとおり Add メソッドで行います。Add を呼び出すたびに、要素は末尾に追加されていきます。

リスト1 Listを生成してAddで追加する

private void button15_Click(object sender, EventArgs e)
{
    //動的な要素の変更
    var list = new List<string>();
    list.Add("AAA");
    list.Add("BBB");
}

図2 Addで「AAA」「BBB」を順番に追加する

図2 Addで「AAA」「BBB」を順番に追加する

この時点で list の中身は「AAA」「BBB」の2件です。

AddRange:複数の要素をまとめて末尾に追加する

Add が1件ずつ追加するのに対して、AddRange は複数の要素をまとめて追加できるメソッドです。list.addra あたりまで入力すると、インテリセンスに AddRange が表示されます。

図3 インテリセンスに表示されるAddRange

図3 インテリセンスに表示されるAddRange

たとえば「CCC」「DDD」という2つの要素を持つ配列 hai があり、その中身を丸ごと list に入れたい場合は、AddRange の引数に配列をそのまま渡します。

リスト2 AddRangeで配列の中身をまとめて追加する

var list = new List<string>();
list.Add("AAA");
list.Add("BBB");

var hai = new string[] { "CCC", "DDD" };
list.AddRange(hai);

list.AddRange(hai); の行にブレークポイントを置いて実行し、ウォッチウィンドウで確認してみます。AddRange を実行する直前は、list が「AAA」「BBB」の2件、hai が「CCC」「DDD」の2件です。この hai の中身を list の末尾に入れたい、というのが今回やりたいことです。

図4 AddRange実行前:listは2件、haiは「CCC」「DDD」の2件

図4 AddRange実行前:listは2件、haiは「CCC」「DDD」の2件

1行ステップ実行すると、「CCC」と「DDD」が list の末尾にまとめて追加され、Count が4になります。

図5 AddRange実行後:末尾に「CCC」「DDD」が追加されてCountが4になる

図5 AddRange実行後:末尾に「CCC」「DDD」が追加されてCountが4になる

このように、2つの配列やコレクションを合体させたいときには AddRange が非常に便利です。コードにもコメントを残しておきます。

リスト3 AddRangeの用途をコメントに残す

var hai = new string[] { "CCC", "DDD" };
list.AddRange(hai);
//2つの配列を合体させるなどが簡単にできる

Insert:指定したインデックスに挿入する

次は Insert です。Insert は、指定したインデックスの位置に要素を入れるメソッドです。第1引数にインデックス(index)、第2引数に入れたい要素(item)を指定します。

図6 Insertのシグネチャ:Insert(int index, string item)

図6 Insertのシグネチャ:Insert(int index, string item)

ここでは「インデックスの1番目に EEE を入れてね」という指定をしてみます。

リスト4 インデックス1にEEEを挿入する

//実行前: AAA, BBB, CCC, DDD
list.Insert(1, "EEE");
//実行後: AAA, EEE, BBB, CCC, DDD

実行前の list は「AAA」「BBB」「CCC」「DDD」の4件で、インデックス1には「BBB」が入っています。ここで Insert を実行すると、「BBB」があった場所に「EEE」が割り込み、もともとあった「BBB」以降の要素は1つずつ後ろにずれます。Count は5になります。

図7 Insert(1, "EEE")実行後:インデックス1に「EEE」が入りCountが5になる

図7 Insert(1, “EEE”)実行後:インデックス1に「EEE」が入りCountが5になる

もう一度、同じインデックス1に今度は「FFF」を挿入してみます。

リスト5 同じインデックス1にFFFを挿入する

list.Insert(1, "EEE");
list.Insert(1, "FFF");
//実行後: AAA, FFF, EEE, BBB, CCC, DDD

直前まではインデックス1が「EEE」でしたが、今度は「FFF」がインデックス1になり、「EEE」以降の要素がすべて後ろにずれます。Insert は既存の要素を上書きするのではなく、割り込ませて後ろを全部ずらす、という動きである点を押さえておいてください。

図8 Insert(1, "FFF")実行後:「FFF」がインデックス1になり、以降が後ろにずれてCountが6になる

図8 Insert(1, “FFF”)実行後:「FFF」がインデックス1になり、以降が後ろにずれてCountが6になる

InsertRange:複数の要素をまとめて挿入する

Add に対する AddRange と同じように、Insert にも InsertRange があります。配列などをまとめて、指定したインデックスの位置に挿入できます。

今度は「GGG」「HHH」「FFF」という3つの要素を持つ配列 hai2 を用意します。3つ目は、このあとの Remove の説明で使うために、あえてすでに list に入っている「FFF」と同じ値にしておきます。

リスト6 InsertRangeでインデックス3に配列をまとめて挿入する

list.Insert(1, "EEE");
list.Insert(1, "FFF");
var hai2 = new string[] { "GGG", "HHH", "FFF" };
list.InsertRange(3, hai2);

図9 InsertRange(3, hai2)を記述する

図9 InsertRange(3, hai2)を記述する

InsertRange の第1引数は挿入を開始するインデックス、第2引数は挿入するコレクションです。「3番目から hai2 の中身を入れてね」という意味になります。実行直前の list は6件で、インデックス3には「BBB」が入っています。

図10 InsertRange実行前:要素は6件、インデックス3は「BBB」

図10 InsertRange実行前:要素は6件、インデックス3は「BBB」

ステップ実行すると、インデックス3から「GGG」「HHH」「FFF」の3つがまとめて入り、もともとインデックス3にあった「BBB」以降は3つ分後ろにずれます。Count は9になります。

図11 InsertRange実行後:インデックス3から「GGG」「HHH」「FFF」が入る

図11 InsertRange実行後:インデックス3から「GGG」「HHH」「FFF」が入る

リスト7 InsertRange前後のlistの中身

//実行前(6件): AAA, FFF, EEE, BBB, CCC, DDD
list.InsertRange(3, hai2);
//実行後(9件): AAA, FFF, EEE, GGG, HHH, FFF, BBB, CCC, DDD

Remove:指定した値を削除する

ここからは削除系のメソッドです。要素を削除したいときの基本は Remove で、引数に削除したい値を指定します。

リスト8 Removeで値を指定して削除する

list.Remove("FFF");

ここで注目したいのは、いま list には「FFF」が2つ入っているという点です。要素は全部で9件あり、「FFF」はインデックス1とインデックス5にあります。

図12 Remove実行前:「FFF」がインデックス1とインデックス5の2か所にある

図12 Remove実行前:「FFF」がインデックス1とインデックス5の2か所にある

この状態で Remove("FFF") を実行するとどうなるでしょうか。結果は、インデックス1にあった「FFF」だけが消え、もう1つの「FFF」は残ります(前が1つ詰まったので、インデックス4に移動しています)。

図13 Remove実行後:最初の「FFF」だけが消え、インデックス4の「FFF」は残る

図13 Remove実行後:最初の「FFF」だけが消え、インデックス4の「FFF」は残る

つまり Remove は、同じ値が複数ある場合、先頭から探して最初に見つかった1件だけを削除します。該当するものを全部消してくれるわけではないので注意してください。コードにもコメントを書いておきます。

リスト9 Removeの注意点をコメントに残す

//指定した値の削除
//複数ある場合、最初に見つかったやつが消える
list.Remove("FFF");
//実行前(9件): AAA, FFF, EEE, GGG, HHH, FFF, BBB, CCC, DDD
//実行後(8件): AAA, EEE, GGG, HHH, FFF, BBB, CCC, DDD

なお、図のデバッガーに「Remove が返されました: true」と表示されているとおり、Remove は bool を返します。削除できた場合は true、指定した値が見つからなかった場合は false です。

RemoveAt:インデックスを指定して削除する

RemoveAt は、値ではなくインデックスを指定して削除するメソッドです。たとえば0番目を指定してみます。

リスト10 RemoveAtでインデックスを指定して削除する

list.RemoveAt(0);

実行前の list は8件で、インデックス0は「AAA」です。

図14 RemoveAt実行前:インデックス0は「AAA」、Countは8

図14 RemoveAt実行前:インデックス0は「AAA」、Countは8

実行すると0番目の「AAA」が消え、後ろの要素が1つずつ前に詰まります。全体は7件になります。

図15 RemoveAt(0)実行後:「AAA」が消えてCountが7になる

図15 RemoveAt(0)実行後:「AAA」が消えてCountが7になる

RemoveRange:範囲を指定してまとめて削除する

RemoveRange は、範囲を指定してまとめて削除するメソッドです。第1引数に削除を開始するインデックス(index)、第2引数に削除する個数(count)を指定します。ここでは「3番目から2個」という指定にします。

リスト11 RemoveRangeで範囲を指定して削除する

//実行前(7件): EEE, GGG, HHH, FFF, BBB, CCC, DDD
list.RemoveRange(3, 2);
//実行後(5件): EEE, GGG, HHH, CCC, DDD

実行前は7件あり、インデックス3から2個なので、「FFF」と「BBB」が消えるはずです。

図16 RemoveRange実行前:インデックス3からの2件(「FFF」「BBB」)が削除対象

図16 RemoveRange実行前:インデックス3からの2件(「FFF」「BBB」)が削除対象

実行すると、予想どおり「FFF」と「BBB」が消え、全体は5件になりました。第2引数は「終了インデックス」ではなく「個数」である点に注意してください。

図17 RemoveRange(3, 2)実行後:「FFF」「BBB」が消えてCountが5になる

図17 RemoveRange(3, 2)実行後:「FFF」「BBB」が消えてCountが5になる

RemoveAll:条件に一致する要素をすべて削除する

RemoveAll は、条件にヒットした要素をすべて削除するメソッドです。シグネチャを見ると、引数は Predicate<string> になっています。Predicate<T> は「T型の値を1つ受け取って bool を返す」デリゲートなので、bool を返す条件をラムダ式で書けばよい、ということになります。

図18 RemoveAllの引数はPredicate<string>

図18 RemoveAllの引数はPredicate<string>

ラムダ式の x には list の要素が1つずつ渡されます。ここでは Contains を使って「Y という文字が含まれているもの」という条件にします。

リスト12 RemoveAllでラムダ式の条件に一致する要素を削除する

list.RemoveAll(x => x.Contains("Y"));

このままでは「Y」を含む要素が1つもないので、動きを確認するために、配列 hai の「DDD」と配列 hai2 の「HHH」の後ろに「Y」を付けておきます。

リスト13 確認用に「DDD」「HHH」の後ろにYを付ける

var hai = new string[] { "CCC", "DDDY" };
list.AddRange(hai);
//2つの配列を合体させるなどが簡単にできる

list.Insert(1, "EEE");
list.Insert(1, "FFF");
var hai2 = new string[] { "GGG", "HHHY", "FFF" };
list.InsertRange(3, hai2);

これで実行します。RemoveAll の直前では要素が5件あり、インデックス2の「HHHY」とインデックス4の「DDDY」に「Y」が含まれています。

図19 RemoveAll実行前:インデックス2「HHHY」とインデックス4「DDDY」にYが含まれる

図19 RemoveAll実行前:インデックス2「HHHY」とインデックス4「DDDY」にYが含まれる

ステップ実行すると、「Y」が付いていた2つの要素がどちらも消え、3件になりました。Remove が「最初に見つかった1件だけ」だったのに対し、RemoveAll は条件に一致するものを全部削除してくれます。

図20 RemoveAll実行後:Yを含む要素がすべて消えてCountが3になる

図20 RemoveAll実行後:Yを含む要素がすべて消えてCountが3になる

リスト14 RemoveAll前後のlistの中身

//実行前(5件): EEE, GGG, HHHY, CCC, DDDY
list.RemoveAll(x => x.Contains("Y"));
//実行後(3件): EEE, GGG, CCC

Clear:すべての要素を削除する

最後は Clear です。全部消したい場合は Clear を呼び出します。引数はありません。

リスト15 Clearですべての要素を削除する

list.Clear();

直前の時点では要素が3件ありますが、Clear を通過すると Count は0になります。

図21 Clear実行後:Countが0になる

図21 Clear実行後:Countが0になる

まとめ

最後に、削除系のメソッドにそれぞれ「何を指定して削除するのか」をコメントで書き添えて整理します。完成したコード全体は次のとおりです。

リスト16 button15_Click の完成コード

private void button15_Click(object sender, EventArgs e)
{
    //動的な要素の変更
    var list = new List<string>();
    list.Add("AAA");
    list.Add("BBB");

    var hai = new string[] { "CCC", "DDDY" };
    list.AddRange(hai);
    //2つの配列を合体させるなどが簡単にできる

    list.Insert(1, "EEE");
    list.Insert(1, "FFF");
    var hai2 = new string[] { "GGG", "HHHY", "FFF" };
    list.InsertRange(3, hai2);

    //指定した値の削除
    //複数ある場合、最初に見つかったやつが消える
    list.Remove("FFF");//指定した値
    list.RemoveAt(0);//インデックス指定
    list.RemoveRange(3, 2);//範囲指定
    list.RemoveAll(x => x.Contains("Y"));//ラムダ指定
    list.Clear();//全削除
}

図22 削除系メソッドにコメントを書き添えた完成コード

図22 削除系メソッドにコメントを書き添えた完成コード

今回扱った9つのメソッドを表にまとめます。

メソッド 分類 指定するもの 動作
Add 追加 要素 末尾に1件追加する
AddRange 追加 コレクション 末尾にまとめて追加する
Insert 挿入 インデックス、要素 指定位置に1件挿入し、以降を後ろにずらす
InsertRange 挿入 インデックス、コレクション 指定位置にまとめて挿入し、以降を後ろにずらす
Remove 削除 値 最初に見つかった1件だけを削除する
RemoveAt 削除 インデックス 指定位置の1件を削除する
RemoveRange 削除 開始インデックス、個数 指定範囲をまとめて削除する
RemoveAll 削除 条件(ラムダ式) 条件に一致する要素をすべて削除する
Clear 削除 なし すべての要素を削除する

Remove は指定した値、RemoveAt はインデックス指定、RemoveRange は範囲指定、RemoveAll はラムダ指定、Clear は全削除、という使い分けになります。配列では手間のかかった要素の増減も、List<T> ならこれらのメソッドを呼び出すだけで済みます。場面に応じて使い分けながら、要素を操作してみてください。

C#文法:コレクション

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る

A01_はじめに
A02_プロジェクトの作成
B01_配列とは
B02_配列の生成とアクセス
B03_生成と同時に値を設定する
B04_型推論による生成
B05_メソッドの引数などにする場合の注意点
B06_Length
B07_IndexOfでの検索
B08_FindIndexでの検索
B09_Find
B10_Exists
B11_FindAllとFindLast
B12_誤ったコピー
B13_Array.Copy
B14_範囲指定のコピー
B15_Resize
C01_ArrayList
C02_List
C03_List 動的な要素の変更
C04_Listのコンストラクタ
C05_Listのコンストラクタ_Capacity
C06_ListTからArrayクラスのメソッドが呼ばれている
D01_コレクションインタフェースとは
D02_異なるコレクションクラスに互換性を持たせる
D03_インタフェースの階層構造
D04_クライアントができる操作を制限させる
D05_クライアントができる操作を制限させる_後半
D06_Enumerableの拡張メソッドに関して
D07_ReadOnly系のコレクションインタフェース
D08_AsReadOnly
D09_ToListでコピーする
D10_ListTはprivateで使う

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る