匿名方法 (C# 2.0) 與 Lambda (C# 3.0) 形成閉包,可存取區域變數與類別成員。編譯器實際生成一個私有輔助類別,將需捕捉的區域變數「提升」為該類別的公開欄位,委托指向該實例的方法。
List<Item> batchItems = new List<Item>();
foreach (var item in ...) {
batchItems.Add(item);
if (batchItems.Count > 1000) {
ThreadPool.QueueUserWorkItem(o => {
var db = new DataContext();
db.Items.InsertAllOnSubmit(batchItems); // 捕捉 batchItems
db.SubmitChanges();
});
batchItems = new List<Item>(); // 立即重新指派!
}
}
錯誤現象:資料遺失、例外亂噴。
編譯器生成輔助類別 AutoGeneratedClass,欄位 m_batchItems。Lambda 委托與主迴圈共用同一實例。主迴圈在排入執行緒池後 立即重新指派 batchItems = new List...,導致 m_batchItems 指向新列表。執行緒池稍後執行時讀取的是「當前」指向的列表,造成:
class WrapperClass {
private List<Item> m_items;
public WrapperClass(List<Item> items) { m_items = items; }
public void WaitCallback(object o) {
var db = new DataContext();
db.Items.InsertAllOnSubmit(m_items);
db.SubmitChanges();
}
}
// 使用
ThreadPool.QueueUserWorkItem(new WrapperClass(batchItems).WaitCallback);
batchItems = new List<Item>();
每次建立新 WrapperClass 實例,鎖定該批次資料,避免共用變更。
若 Lambda 建立運算式樹 (Expression<Func...>) 並捕捉區域變數,執行/解析時取得的是「當前」值,非建立時值。
Java 要求捕捉的區域變數必須 final,編譯期防止後續修改,從語言層面杜絕此類問題。