none
foreachとList.ForEachの動作の違い RRS feed

  • 質問

  • 以下のコードを実行すると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
    2009年6月15日 9:46

回答

  • 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
    2009年6月15日 11:23
  • 匿名デリゲートで使用されている変数がどのように処理されるかといったところかな?

    List<T>.ForEach は引数で l を渡し、foreach は(どのループでも同じ)メンバー変数で l を渡すと考えれば納得できますか?
    匿名デリゲートが実際にどのような実装(コンパイル結果)になっているかは、簡単なサンプルを作ってIL逆アセンブラー(ILDASM)で読んでみると良いでしょう。


    匿名デリゲートはコンパイル時に独自のクラスに展開されます。foreachの場合、ループの外でそのインスタンスを1回だけ生成し、
    ループ中は同じインスタンスを再利用し、そのクラスのメンバー変数に対して繰り返し代入します。
    QueueUserWorkItemで登録されるものは、その同じインスタンスのメソッドのデリゲートであるため、期待通りの結果にはなりません。
    解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。
    • 回答としてマーク TakeF 2009年6月16日 4:16
    2009年6月15日 11:54
    モデレータ
  • 理由はすでに他の回答者の方々が答えられていますので、いまさらながらのレスですが、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
    2009年6月15日 14:49
  • 何をキャプチャーするのかわかっていれば

    foreach( var list in parentList ){
      var l = list;
      ThreadPool.QueueUserWorkItem( o => {
        foreach( string s in l )
          Console.WriteLine( "foreach:" + s );
        } );
    }

    こっちの方が本質的です。
    • 回答としてマーク TakeF 2009年6月16日 4:16
    2009年6月15日 15:42
  • 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
    2009年6月16日 1:31

すべての返信

  • 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
    2009年6月15日 11:23
  • 匿名デリゲートで使用されている変数がどのように処理されるかといったところかな?

    List<T>.ForEach は引数で l を渡し、foreach は(どのループでも同じ)メンバー変数で l を渡すと考えれば納得できますか?
    匿名デリゲートが実際にどのような実装(コンパイル結果)になっているかは、簡単なサンプルを作ってIL逆アセンブラー(ILDASM)で読んでみると良いでしょう。


    匿名デリゲートはコンパイル時に独自のクラスに展開されます。foreachの場合、ループの外でそのインスタンスを1回だけ生成し、
    ループ中は同じインスタンスを再利用し、そのクラスのメンバー変数に対して繰り返し代入します。
    QueueUserWorkItemで登録されるものは、その同じインスタンスのメソッドのデリゲートであるため、期待通りの結果にはなりません。
    解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。
    • 回答としてマーク TakeF 2009年6月16日 4:16
    2009年6月15日 11:54
    モデレータ
  • 理由はすでに他の回答者の方々が答えられていますので、いまさらながらのレスですが、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
    2009年6月15日 14:49
  • 何をキャプチャーするのかわかっていれば

    foreach( var list in parentList ){
      var l = list;
      ThreadPool.QueueUserWorkItem( o => {
        foreach( string s in l )
          Console.WriteLine( "foreach:" + s );
        } );
    }

    こっちの方が本質的です。
    • 回答としてマーク TakeF 2009年6月16日 4:16
    2009年6月15日 15:42
  • 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
    2009年6月16日 1:31
  • 返信頂いた皆様、どうもありがとうございます。

    foreachとForEachのListに対するアクセスの違い、
    匿名デリゲートの変数に対するアクセス方。
    どちらも興味深く理解させて頂きました。

    特に匿名デリゲートは何気なく使っていたのですが、動作の仕組みを理解しておかないと
    予想外の動作をする事がありそうですねぇ。

    もう少し勉強してみようと思います。
    2009年6月16日 4:37