C#でListの内の要素をforeach中に削除する

結論から言うと、foreachの中でリストの要素を削除するのは無理です。諦めましょう。*1

削除しようとすると以下のように例外が起きます。

// これは無理、例外が起きる
foreach (Seafood item in list)
{
    if (item.Taste == Taste.Bad)
    {
        list.Remove(item);
        // 例外発生!!
        // 型 'System.InvalidOperationException' の
        // ハンドルされていない例外が mscorlib.dll で発生しました
        // 追加情報: コレクションが変更されました。列挙操作は実行されない可能性があります。
    }
}

代替手段

foreachで削除するのは諦めて代わりの方法を探します。

以降で使用するクラスを先に記載します。

// リストに入れるクラス
public class Seafood
{
    public string Name { get; set; }
    public Taste Taste { get; set; }
}
// リストに入れるクラスが使う列挙子
public enum Taste { Unknwon = 0, Nice, Ordinary, Bad };

// -----

// こんな感じでデータを作成している
var list = new List<Seafood>()
{
    new Seafood() { Name = "ika",    Taste = Taste.Bad  },
    new Seafood() { Name = "tako",   Taste = Taste.Bad  },
    new Seafood() { Name = "hotate", Taste = Taste.Bad  },
    new Seafood() { Name = "akagai", Taste = Taste.Nice },
};

(1) for文を逆順で回す

言語を問わず最も一般的な方法です。

// (1) forの逆順でアクセス(昔からある伝統的な方法
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i].Taste == Taste.Bad)
    {
        list.RemoveAt(i); // Remove(list[i])は使わない事。
    }
}

(2) RemoveAllメソッドで削除

List.RemoveAllにラムダ式で削除条件を指定して削除します。

ほんの少しだけforの逆順より早く動作します。

// (2) RemoveAllで条件を指定:1割程度(1)より高速
list.RemoveAll(p => p.Taste == Taste.Bad);

(3) Whereで抜粋する

Listに定義された拡張メソッドのWhereを使用し、「元のリストはそのまま」で新しく条件に一致するリスト(本当はシーケンスですが…)を取得します。

// Whereで新しいリストを取得
IEnumerable<Seafood> newList =list.Where(p => p.Taste != Taste.Bad);

// 補足:但しこの後newListを使ってリストを実際に削除しようとするとエラー
foreach (Seafood item in newList)
{
    list.Remove(item);
    // 例外発生!!
    // 型 'System.InvalidOperationException' のハンドルされていない例外が mscorlib.dll で発生しました
    // 追加情報: コレクションが変更されました。列挙操作は実行されない可能性があります。
}

Listに対して条件付きのイテレーターを作成するだけで、評価自体は遅延実行(=呼び出した時にコストが発生)なのでこの操作自体はほぼノーコストで完了します。

繰り返しになりますが、この場合元のListの内容は変化しません。

(4) 自作する

全然おすすめでもなんでもないですが拡張メソッドを用いて自作してみます。RemoceAllなどのシーケンスにContinueやbreakが使えないという文句を言う方がいるので併せて対応します。

// Listの機能を拡張するクラス
public static class ListExtension
{
    // RemoveAllの機能拡張版
    public static void RemoveAllEx<T>(this IList<T> list, Func<T, Loop> func)
    {
        var indexes = new Stack<int>(); // 削除するインデックスを覚えておく

        for (int i = 0; i < list.Count; i++)
        {
            Loop next = func(list[i]);
            if (next == Loop.Remove)
            {
                indexes.Push(i);
            }
            else if (next == Loop.Break) // 処理を中断
            {
                break;
            }
        }

        foreach (int i in indexes)
        {
            list.RemoveAt(i); // 最後にまとめて消す
        }
    }
}

// ループ処理を継続するかどうか
public enum Loop
{
    Continue = 0, // 続ける
    Break,        // 終了する
    Remove,       // 削除する
}

使い方は以下の通り。

// 先頭から100件以内ののTasteがBadの要素を全部削除
int cnt = 0;
list.RemoveAllEx(p =>
{
    if (cnt > 100) // 中断条件
    {
        return Loop.Break;
    }
    else if (p.Taste == Taste.Bad) // 削除条件
    {
        return Loop.Remove;
    }

    return Loop.Continue; // 基本処理を続ける
});

作っておいてなんですが、(1)のforの逆順の方がいいですね。動作も3倍くらい遅いです。

foreachの右辺でToArray() + Removeはやめましょう

QiitaにToArrayすれば削除できると書かれていましたが、この記事のように書いてはいけません。

別に動くんですが、メモリが最悪倍必要になったり、Remove()はリスト内を検索してO(N)の計算量なので、削除量が多いととんでもなく遅くなります。(全件削除で数百倍遅い。

// 一旦ToArrayしてからforeachで削除する
foreach (Class item in list.ToArray()/*メモリ倍化*/)
{
    if (/*削除条件*/)
    {
        list.Remove(item); // これが滅茶苦茶遅い
    }
}       

因みに、List.ToArray()の実装はは以下の通り。

抜粋していますが、新しい配列を宣言して中身を全部コピーしています。値型のオブジェクトだと丸ごとコピーでメモリ倍です。

// ToArrayの実装(抜粋)
// ReferenceSource:http://urx2.nu/MHMs
public T[] ToArray() 
{
    T[] array = new T[_size];
    Array.Copy(_items, 0, array, 0, _size);
    return array;
}

Qiitaさん…

*1:通常の方法だとね。