トップ回答者
foreachとList.ForEachの動作の違い

質問
-
以下のコードを実行するとVS2008においてforeachとList.ForEachで動作が異なります。
List.ForEachは期待通りの動作をするのですが、foreachの動作が予想と異なります。
何故このような結果になるのかご存知の方がおられましたら教えて頂けないでしょうか?
よろしくお願いします。
<テストコード>
using System;
using System.Collections.Generic;
using System.Threading;
namespace TestConsoleApp
{
class Program
{
static void Main(string[] args)
{
List<string> list1 = new List<string>();
List<string> list2 = new List<string>();
List<string> list3 = new List<string>();
list1.Add("1");
list1.Add("2");
list1.Add("3");
list2.Add("a");
list2.Add("b");
list2.Add("c");
list3.Add("A");
list3.Add("B");
list3.Add("C");
List<List<string>> parentList = new List<List<string>>();
parentList.Add(list1);
parentList.Add(list2);
parentList.Add(list3);
foreach (List<string> l in parentList)
{
ThreadPool.QueueUserWorkItem(delegate
{
foreach (string s in l)
Console.WriteLine("foreach:" + s);
});
}
parentList.ForEach(delegate(List<string> l)
{
ThreadPool.QueueUserWorkItem(delegate
{
foreach (string s in l)
Console.WriteLine("ForEach:" + s);
});
});
Console.ReadLine();
}
}
}
<動作結果>
foreach:A
foreach:B
foreach:C
foreach:A
foreach:B
foreach:C
foreach:A
foreach:B
foreach:C
ForEach:1
ForEach:2
ForEach:3
ForEach:a
ForEach:b
ForEach:c
ForEach:A
ForEach:B
ForEach:C
回答
-
List<T>.ForEachはAction<T>が呼び出されるときにListから取り出します。
いつ、どのスレッドで実行されるかわからないThreadPoolでも、呼び出されたときに順番にListから取り出します。
foreachはIEnumerator.MoveNext()でListから取り出されます。
いつ、どのスレッドで実行されるかわからないThreadPoolでdelegateが呼ばれたときには、すでにList<string> lは別のList<string>を参照しています。- 回答としてマーク TakeF 2009年6月16日 4:16
-
匿名デリゲートで使用されている変数がどのように処理されるかといったところかな?
List<T>.ForEach は引数で l を渡し、foreach は(どのループでも同じ)メンバー変数で l を渡すと考えれば納得できますか?
匿名デリゲートが実際にどのような実装(コンパイル結果)になっているかは、簡単なサンプルを作ってIL逆アセンブラー(ILDASM)で読んでみると良いでしょう。
匿名デリゲートはコンパイル時に独自のクラスに展開されます。foreachの場合、ループの外でそのインスタンスを1回だけ生成し、
ループ中は同じインスタンスを再利用し、そのクラスのメンバー変数に対して繰り返し代入します。
QueueUserWorkItemで登録されるものは、その同じインスタンスのメソッドのデリゲートであるため、期待通りの結果にはなりません。
解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。- 回答としてマーク TakeF 2009年6月16日 4:16
-
理由はすでに他の回答者の方々が答えられていますので、いまさらながらのレスですが、foreach で期
待通り(注)にするには以下のようにすればよいはずです。お試しください。foreach (List<string> l in parentList)
{
ThreadPool.QueueUserWorkItem(
delegate(Object stateInfo)
{
foreach (string s in (List<string>)stateInfo)
Console.WriteLine("foreach:" + s);
},
l);
}(注)実行の順番は期待通りにはならないと思います。TakeF さんがアップされている <動作結果> で
順番に並んでいるのはたまたまでしょう。- 回答としてマーク TakeF 2009年6月16日 4:16
-
foreach (List<string> l in parentList){ThreadPool.QueueUserWorkItem(delegate{foreach (string s in l)Console.WriteLine("foreach:" + s);});}「QueueUserWorkItemでキューに入れた匿名関数が実行される際、変数"l"に何が入っているか。」ということです。書き込んで頂いた動作結果では、lにはparentListの最後の要素が入っています。下のようにすると、parentList.ForEach(delegate(List<string> l) { ... } と同じ結果になります。foreach (List<string> l in parentList){ThreadPool.QueueUserWorkItem(delegate(object state){foreach (string s in (state as List<string>))Console.WriteLine("foreach:" + s);}, l);}
- 回答としてマーク TakeF 2009年6月16日 4:16
すべての返信
-
List<T>.ForEachはAction<T>が呼び出されるときにListから取り出します。
いつ、どのスレッドで実行されるかわからないThreadPoolでも、呼び出されたときに順番にListから取り出します。
foreachはIEnumerator.MoveNext()でListから取り出されます。
いつ、どのスレッドで実行されるかわからないThreadPoolでdelegateが呼ばれたときには、すでにList<string> lは別のList<string>を参照しています。- 回答としてマーク TakeF 2009年6月16日 4:16
-
匿名デリゲートで使用されている変数がどのように処理されるかといったところかな?
List<T>.ForEach は引数で l を渡し、foreach は(どのループでも同じ)メンバー変数で l を渡すと考えれば納得できますか?
匿名デリゲートが実際にどのような実装(コンパイル結果)になっているかは、簡単なサンプルを作ってIL逆アセンブラー(ILDASM)で読んでみると良いでしょう。
匿名デリゲートはコンパイル時に独自のクラスに展開されます。foreachの場合、ループの外でそのインスタンスを1回だけ生成し、
ループ中は同じインスタンスを再利用し、そのクラスのメンバー変数に対して繰り返し代入します。
QueueUserWorkItemで登録されるものは、その同じインスタンスのメソッドのデリゲートであるため、期待通りの結果にはなりません。
解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。- 回答としてマーク TakeF 2009年6月16日 4:16
-
理由はすでに他の回答者の方々が答えられていますので、いまさらながらのレスですが、foreach で期
待通り(注)にするには以下のようにすればよいはずです。お試しください。foreach (List<string> l in parentList)
{
ThreadPool.QueueUserWorkItem(
delegate(Object stateInfo)
{
foreach (string s in (List<string>)stateInfo)
Console.WriteLine("foreach:" + s);
},
l);
}(注)実行の順番は期待通りにはならないと思います。TakeF さんがアップされている <動作結果> で
順番に並んでいるのはたまたまでしょう。- 回答としてマーク TakeF 2009年6月16日 4:16
-
foreach (List<string> l in parentList){ThreadPool.QueueUserWorkItem(delegate{foreach (string s in l)Console.WriteLine("foreach:" + s);});}「QueueUserWorkItemでキューに入れた匿名関数が実行される際、変数"l"に何が入っているか。」ということです。書き込んで頂いた動作結果では、lにはparentListの最後の要素が入っています。下のようにすると、parentList.ForEach(delegate(List<string> l) { ... } と同じ結果になります。foreach (List<string> l in parentList){ThreadPool.QueueUserWorkItem(delegate(object state){foreach (string s in (state as List<string>))Console.WriteLine("foreach:" + s);}, l);}
- 回答としてマーク TakeF 2009年6月16日 4:16