理解C#中的閉包


閉包的概念

內層的函數可以引用包含在它外層的函數的變量,即使外層函數的執行已經終止。但該變量提供的值並非變量創建時的值,而是在父函數范圍內的最終值

閉包的優點

使用閉包,我們可以輕松的訪問外層函數定義的變量,這在匿名方法中普遍使用。比如有如下場景,在winform應用程序中,我們希望做這么一個效果,當用戶關閉窗體時,給用戶一個提示框。我們將添加如下代碼:

private void Form1_Load(object sender, EventArgs e)
{
       string tipWords = "您將關閉當前對話框";
       this.FormClosing += delegate
       {
            MessageBox.Show(tipWords);
       };
}

若不使用匿名函數,我們就需要使用其他方式來將tipWords變量的值傳遞給FormClosing注冊的處理函數,這就增加了不必要的工作量。

閉包陷阱

應用閉包,我們要注意一個陷阱。比如有一個用戶信息的數組,我們需要遍歷每一個用戶,對各個用戶做處理后輸出用戶名。

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, zhangzetian

但是實際我們運行后發現,程序會報錯,提示索引超出界限。

為什么沒有達到我們預期的效果呢?讓我們再來看一下閉包的概念。內層函數引用的外層函數的變量的最終值。就是說,當線程中執行方法時,方法中的i參數的值,始終是userList.Count。原來如此,那我們該如何

避免閉包陷阱呢?C#中普遍的做法是,將匿名函數引用的變量用一個臨時變量保存下來,然后在匿名函數中使用臨時變量。

 

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.注意,每次的輸出順序可能不同。

NET編譯器與閉包

提出了問題,給出了解決方案,我們總算知道該怎么正確使用閉包了。但是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 的提醒。版本已經更正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM