タイトルそのままです。C#でListの内の要素を削除したときに時々例外が発生するらしくアプリが落ちることがありました。常にではなく「時々」です。このようなバグは改善するのが難しいです。
時々例外が発生する理由は「C#でListの内の要素をforeachのなかで削除しようとしていたから」です。
Contents
foreach文のなかでリストの要素の追加と削除は不可
まずは明らかにダメな例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { List<int> vs = new List<int>(); for (int i = 0; i < 10; i++) vs.Add(i); foreach (int i in vs) { if (i % 2 == 0) vs.Remove(i); } } } |
System.InvalidOperationException: コレクションが変更されました。列挙操作は実行されない可能性があります。
このような例外が発生します。foreach文のなかではリストの要素を追加したり削除することはできないのです。
単純にリストのなかから偶数の要素を取り除きたいだけならforeach文を使わずにこんな方法でもできます。
1 2 3 4 5 6 |
List<int> vs = new List<int>(); for (int i = 0; i < 10; i++) vs.Add(i); vs = vs.Where(_ => _ % 2 != 0).ToList(); |
同期処理なら鳩でもミスはしない
ただ別のところでforeach文を使う場合もあるはずです。これが通常の同期処理(複数のタスクを実行する際に一つずつ順番にタスクが実行される)場合であれば問題はありません。たとえば上記のコードであれば、リストから要素を取り除いたあとにforeach文を使ったとしてもなんの問題もありません。
1 2 3 4 5 6 7 8 9 10 11 |
List<int> vs = new List<int>(); for (int i = 0; i < 10; i++) vs.Add(i); vs = vs.Where(_ => _ % 2 != 0).ToList(); foreach (int i in vs) { // 要素を取り除いたあとにforeach文を使うのであればなんの問題もない } |
非同期処理をしているときは注意!
ところが非同期処理というものを覚えてこれを多用しはじめると問題がおきます。別のところでforeach文でリストの要素を取得して処理をしているのに、非同期でおこなわれている処理で要素が削除されたらどうなるのでしょうか?
つまりFunc1メソッドが呼び出されてから10秒以内にFunc2メソッドが呼び出してしまった場合です。これは実験してみればわかりますが、さきほどと同じ例外(System.InvalidOperationException: コレクションが変更されました。列挙操作は実行されない可能性があります)が発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
List<int> Vs = new List<int>(); void SetValueToList() { for (int i = 0; i < 10; i++) Vs.Add(i); } async void Loop() { foreach (int i in Vs) { await Task.Delay(1000); // 時間がかかる処理 } } private void Func1() { SetValueToList(); Loop(); } private void Func2() { Vs.RemoveAt(0); } |
非同期処理時の問題回避策?
ではリストに格納されているデータをforeach文で処理したいけど例外が発生しては困るという場合、どうすればいいのでしょうか?
こんな方法はどうでしょうか?
別のリストに要素をコピーしてからループを回します。この場合、Vsの要素が削除されてもvsの要素に変更はないので例外は発生しません。
1 2 3 4 5 6 7 8 9 10 11 |
async void Loop() { List<int> vs = new List<int>(Vs); foreach (int i in vs) { await Task.Delay(1000); // 時間がかかる処理 // 途中でVsの要素が追加または削除されても問題はない } } |
時間的ロスはどれくらい?
ところで別のリストに要素をコピーするのにどれくらい時間がかかるのでしょうか?
Func1メソッドとFunc2メソッドの実行にかかる時間を比較してみましたが、コピーをしているぶんFunc2メソッドのほうが時間がかかります。10万件のリストに格納された整数の総和を計算するのにかかった時間はFunc1メソッドが0.7~0.8ミリ秒、Func2メソッドが0.9~1.1ミリ秒でした。10万件あっても1ミリ秒に満たない差なので無視してもよいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
void Func1() { List<int> vs = new List<int>(); for (int i = 0; i < 10 * 10000; i++) vs.Add(i); // ループを単純に回す場合とリストのコピーを生成してからループを回す場合の時間を比較する Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); long ret = 0; foreach (int i in vs) ret += i; stopwatch.Stop(); // stopwatch.Elapsed.TotalMilliseconds の値は? } void Func2() { List<int> vs = new List<int>(); for (int i = 0; i < 10 * 10000; i++) vs.Add(i); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); List<int> vs2 = new List<int>(vs); long ret = 0; foreach (int i in vs2) ret += i; stopwatch.Stop(); // stopwatch.Elapsed.TotalMilliseconds の値は? } |