匿名函式陷阱

在大量使用 Lambda 語法後, 在許多程式碼中都會藏著大量的匿名函式, 這種函式內部包裹函式的寫法又稱為 Closure(閉包), 進一步的了解可以參考忠成哥寫的 The Closure and Lambda Programming Style . 但是這種寫法存在一些陷阱, 我得老實說, 其實這陷阱不是C# 編譯器的錯, 而是大部分踩進這陷阱的人通常是沒有仔細思考其中的緣故罷了.
 

        在大量使用 Lambda 語法後, 在許多程式碼中都會藏著大量的匿名函式, 這種函式內部包裹函式的寫法又稱為 Closure(閉包), 進一步的了解可以參考忠成哥寫的 The Closure and Lambda Programming Style . 但是這種寫法存在一些陷阱, 我得老實說, 其實這陷阱不是C# 編譯器的錯, 而是大部分踩進這陷阱的人通常是沒有仔細思考其中的緣故罷了.

 

 

        先看看以下的程式碼, 你認為最後的結果是甚麼 ?

        private void button1_Click(object sender, EventArgs e)
        {
            Action action = null;
            for(int i =0; i< 10;i++)
            {
                action += new Action
                    (delegate()
                {
                    Debug.WriteLine(i.ToString ());
                }
                    );
            }

            action();       
        }

        答案是一串 10, 如下圖 :

        2015-05-25_19-53-18

 

 

    

        上面的程式要如何才會得到正確的結果 ? 其中一個是拿掉委派的累加, 然後將 action(); 移入迴圈內, 當然, 這程式碼看來有點無聊, 因為它壓根兒可以不用匿名函式.

        private void button2_Click(object sender, EventArgs e)
        {
            Action action = null;
            for (int i = 0; i < 10; i++)
            {
                action = new Action
                    (delegate()
                    {
                        Debug.WriteLine(i.ToString());
                    }
                    );
                action();
            }
        }

 

 

        接著要來說明在多執行緒中使用匿名函式的陷阱. 咱們先來瞧瞧以下的程式碼:

        private void button1_Click(object sender, EventArgs e)
        {
            Thread t = new Thread(Test1);
            t.IsBackground = true;
            t.Start();
        }


       private void Test1()
        {
            for (int i = 0; i < 10; i++)
            {
                this.Invoke(new Action(()=> Debug.WriteLine (i)));
            }
        }

        執行結果非常令人滿意, 乖乖的從 0 ~ 9.

 

 

        如果, 我把程式碼改成這樣呢 ?

        private void button2_Click(object sender, EventArgs e)
        {
            Thread t = new Thread(Test2);
            t.IsBackground = true;
            t.Start();
        }


        private void Test2()
        {
            for (int i = 0; i < 10; i++)
            {
                this.BeginInvoke(new Action(() => Debug.WriteLine(i)));
            }
        }

        不過把 Control.Invoke() 方法改成 Control.BeginInvoke() 方法, 整個狀況又不受控. 結果又變成一串10 (如果你使用的是 WPF, 使用 Dispatcher.Inovoke() 和 Dispatcher.BeginInvoke() 也會對應到 Control.Invoke() 和 Control.BeginInvoke() 一樣的結果). 這個原因在於 Invoke() 和 BeginInvoke() 在執行的方式很不一樣. Invoke 以同步的方式將委派傳送到 UI Thread, 而 BeginInvoke 是以非同步的方式傳送到 UI Thread.

 

 

        到這邊可能就得出一個結論 --  那用 Invoke 就沒這問題了 (或是 BeginInvoke + EndInvoke, 不過 Distpatcher 是沒有 EndInvoke 這玩意的). 這不能說錯, 但是有兩個理由讓我覺得不該這麼簡化這個問題. (1) 誰知道會不會有一版新的 .Net Framework 改變了 Invoke 的作法. (2) 在某些 Framework 裡是沒有 Invoke 或 EndInvoke 的, 例如 Silverlight, 它只有 Dispatcher.BeginInvoke(), 在 Windows Runtime 裡的則是 Dispatcher.RunAsync().

 

 

       所以不論你用 Inovke, BeginInvoke, RunAsync 還是其他甚麼的Invoke, 最佳的方式應該是使用具有參數的委派, 並在傳遞委派時傳送引數過去, 如以下的程式碼:

        private void button3_Click(object sender, EventArgs e)
        {
            Thread t = new Thread(Test3);
            t.IsBackground = true;
            t.Start();
        }


       private void Test3()
        {
            for (int i = 0; i < 10; i++)
            {
                this.BeginInvoke(new Action<int>((int value) => Debug.WriteLine(value)), new object[] {i});
            }
        }

 

 

        以後在使用匿名函數的時候, 發現結果不如預期, 千萬不要以為是甚麼微軟的 bug 還是甚麼不合理的現象囉.