在大量使用 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, 如下圖 :
上面的程式要如何才會得到正確的結果 ? 其中一個是拿掉委派的累加, 然後將 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 還是甚麼不合理的現象囉.