內層的函數能夠引用包含在它外層的函數的變量,即便外層函數的執行已經終止。但該變量提供的值並不是變量建立時的值,而是在父函數範圍內的最終值。數組
使用閉包,咱們能夠輕鬆的訪問外層函數定義的變量,這在匿名方法中廣泛使用。好比有以下場景,在winform應用程序中,咱們但願作這麼一個效果,當用戶關閉窗體時,給用戶一個提示框。咱們將添加以下代碼:閉包
private void Form1_Load(object sender, EventArgs e) { string tipWords = "您將關閉當前對話框"; this.FormClosing += delegate { MessageBox.Show(tipWords); }; }
若不使用匿名函數,咱們就須要使用其餘方式來將tipWords變量的值傳遞給FormClosing註冊的處理函數,這就增長了沒必要要的工做量。函數
應用閉包,咱們要注意一個陷阱。好比有一個用戶信息的數組,咱們須要遍歷每個用戶,對各個用戶作處理後輸出用戶名。this
public class UserModel { public string UserName { get; set; } public int UserAge { get; set; } }
List<UserModel> userList = new List<UserModel> { new UserModel{ UserName="jiejiep", UserAge = 26}, new UserModel{ UserName="xiaoyi", UserAge = 25}, new UserModel{ UserName="zhangzetian", UserAge=24} }; for(int i = 0 ; i < 3; i++) { ThreadPool.QueueUserWorkItem((obj) => { //TODO //Do some process... //... Thread.Sleep(1000); UserModel u = userList[i]; Console.WriteLine(u.UserName); }); }
咱們預期的輸出是, jiejiep, xiaoyi, zhangzetianspa
可是實際咱們運行後發現,程序會報錯,提示索引超出界限。線程
爲何沒有達到咱們預期的效果呢?讓咱們再來看一下閉包的概念。內層函數引用的外層函數的變量的最終值。就是說,當線程中執行方法時,方法中的i參數的值,始終是userList.Count。原來如此,那咱們該如何翻譯
避免閉包陷阱呢?C#中廣泛的作法是,將匿名函數引用的變量用一個臨時變量保存下來,而後在匿名函數中使用臨時變量。code
List<UserModel> userList = new List<UserModel> { new UserModel{ UserName="jiejiep", UserAge = 26}, new UserModel{ UserName="xiaoyi", UserAge = 25}, new UserModel{ UserName="zhangzetian", UserAge=24} }; for(int i = 0 ; i < 3; i++) { UserModel u = userList[i]; ThreadPool.QueueUserWorkItem((obj) => { //TODO //Do some process... //... Thread.Sleep(1000); //UserModel u = userList[i]; Console.WriteLine(u.UserName); }); }
咱們再運行來看,輸出依次爲 jiejiep,xiaoyi, zhangzetian.注意,每次的輸出順序可能不一樣。orm
提出了問題,給出瞭解決方案,咱們總算知道該怎麼正確使用閉包了。可是dotNET是如何實現閉包的呢?執着的程序猿們,不會知足於這種表象的解決方案,讓咱們來看看dotNET是如何實現閉包的。咱們能夠微軟提供的isdasm.exe來查看編譯後的代碼。咱們先來看看有問題的代碼。將IL代碼翻譯後,能夠獲得以下的僞代碼。對象
public class TempClass5 { public List<UserModel> UserList; } public class TempClass8 { public int i = 0; public TempClass5 c5; public ShowMessage(object o) { Thread.Sleep(1000); Console.WriteLine(c5.UserList[i].UserName); } }
public class Program { TempClass5 c55 = new TempClass5(); c55.UserList = new List<UserModel>(); c55.UserList.Add(new UserModel{ UserName="jiejiep", UserAge = 26}); c55.UserList.Add(new UserModel{ UserName="xiaoyi", UserAge = 25}); c55.UserList.Add(new UserModel{ UserName="zhangzetian", UserAge=24}); TempClass8 c8 = new TempClass8(); c8.c5 = c55; WaitCallback callback = c8.ShowMessage; for(int c8.i=0; c8.i < 3; c8.i++) { ThreadPool.QueueUserWorkItem(callback); } }
原來,編譯器爲咱們生成了一個臨時類,該類包含一個 int成員i,一個TempClass5實例c5, 一個實例方法 ShowMessage(object) 。再看看遍歷部分的代碼,咱們頓時就豁然開朗了,原來一直都只有一個 TempClass8實例,遍歷時始終改變的是tempCls對象的i字段的值。因此最後輸出的,始終是最後一個遍歷獲得的元素的 UserName 。
咱們再來看看使用臨時變量後的代碼,編譯器是如何處理的呢。
public class Program { TempClass5 c55 = new TempClass5(); c55.UserList = new List<UserModel>(); c55.UserList.Add(new UserModel{ UserName="jiejiep", UserAge = 26}); c55.UserList.Add(new UserModel{ UserName="xiaoyi", UserAge = 25}); c55.UserList.Add(new UserModel{ UserName="zhangzetian", UserAge=24});
for(int i=0; i < 3; i++) {
TempClass8 c8 = new TempClass8(); c8.c5 = c55;
c8.i = i;
WaitCallback callback = c8.ShowMessage;
ThreadPool.QueueUserWorkItem(callback);
}
}
咱們看到,使用臨時變量這種解決方案時,編譯器至關因而每次遍歷時都實例化了一個 TempClass8對象。因此內層函數引用的c8的i成員始終是遍歷對應的元素。故能有效的解決閉包帶來的陷阱。
寫在文章後面:
這裏感謝 dreamfor 的提醒。版本已經更正。