C#由變量捕獲引起對閉包的思考


前言

偶爾翻翻書籍看看原理性的東西確實有點枯燥,之前有看到園中有位園友說到3-6年工作經驗的人應該了解的.NET知識,其中就有一點是關於C#中的閉包,其實早之前在看書時(之前根本不知道C#中還有閉包這一說)看到對於閉包的內容篇幅很少而且介紹的例子一看就懂(最終也就是有個印象而已),反正工作又用不到來讓你去實現閉包,於是乎自己心存僥幸心理,這兩天心血來潮再次翻了翻書想仔細研究一番(或許是出於內心的惶恐吧,工作幾年竟然不知道閉包,就算知道而且僅止於了解,你是在自欺欺人么),緊接着就查了下資料來研究研究這個東西,若有錯誤之處,請指出。

話題

首先來我們來看看委托的演變進化史,有人問了,本節的主題不是【C#由變量捕獲引起對閉包的思考】?哦,看的還仔細,是的,咱能別着急么,又有人問了,你之前不是寫過有關委托的詳細介紹么,哦,看來還是我的粉絲知道的還挺多,但是這個介紹側重點不同啦,廢話少來,進入主題才是真理。

C#1.0之delegate

我們今天知道過來一個列表可以通過lamda如where或者predicate來實現,而在C#1.0中我們必須來寫一個方法來實現predicate的邏輯,緊接着創建委托實例是通過指定的方法名來創建。我們來創建一個幫助類(ListUtil),如下:

/// <summary>
    /// 操作list幫助類
    /// </summary>
    static class ListUtil
    {
        /// <summary>
        /// 創建一個predicate
        /// </summary>
        public static IList<T> Filter<T>(IList<T> source, Predicate<T> predicate)
        {
            List<T> ret = new List<T>();
            foreach (T item in source)
            {
                if (predicate(item))
                {
                    ret.Add(item);
                }
            }
            return ret;
        }

        /// <summary>
        ///遍歷列表並在控制台上進行打印
        /// </summary>
        public static void Dump<T>(IList<T> list)
        {
            foreach (T item in list)
            {
                Console.WriteLine(item);
            }
            Console.ReadKey();
        }
    }

同時給出一個測試數據:

    static class SampleData
    {
        public static readonly IList<string> Words =
            new List<string> { "the", "quick", "brown", "fox", "jumped",
             "over", "the", "lazy", "dog" }.AsReadOnly();    
    }

現在我們要做的是返回其長度小等於4的字符串並打印,給出長度小於4的方法:

        static bool MatchFourLettersOrFewer(string item)
        {
            return item.Length <= 4;
        }

下面我們在控制台調用上述方法來進行過濾:

  Predicate<string> predicate = new Predicate<string>(MatchFourLettersOrFewer);
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

結果打印如下:

上述一切都是so easy!當我們利用委托來實現時只是簡單的進行一次調用不會多次用到,為了精簡代碼,此時匿名方法出現在C# 2.0.

C#2.0之delegate 

上述在控制台進行調用方法我們稍作修改即可達到同樣效果,如下:

            Predicate<string> predicate = 
                delegate(string item) 
                {
                    return item.Length <= 4;
                };
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

好了,到了這里貌似有點浪費篇幅,到這里我們反觀上述代碼,對於predicate中過濾數據長度都是硬編碼,缺少點什么,我們首先要講的是閉包,那對於閉包需要的可以基本概括為:閉包是函數與其引用環境組合而成的實體(來源於:你必須知道的.NET)。我們可以將其理解為函數與上下文的綜合。我們需要來通過手動輸入過濾數據的長度來給出一個上下文。我們給出一個過濾長度的類(VariableLengthMather):

    public class VariableLengthMatcher
    {
        int maxLength;

        /// <summary>
        /// 將手動輸入的數據進行傳遞
        /// </summary>
        /// <param name="maxLength"></param>
        public VariableLengthMatcher(int maxLength)
        {
            this.maxLength = maxLength;
        }

        /// <summary>
        /// 類似於匿名方法
        /// </summary>
        public bool Match(string item)
        {
            return item.Length <= maxLength;
        }
    }

下面我們來進行手動輸入調用以此來過濾數據:

            Console.Write("Maximum length of string to include? ");
            int maxLength = int.Parse(Console.ReadLine());

            VariableLengthMatcher matcher = new VariableLengthMatcher(maxLength);
            Predicate<string> predicate = matcher.Match;
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

演示如下:

接着我們將上述控制台代碼進行如下改造:

            Console.Write("Maximum length of string to include? ");
            int maxLength = int.Parse(Console.ReadLine());
            VariableLengthMatcher matcher = new VariableLengthMatcher(maxLength);
            Predicate<string> predicate = matcher.Match;
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);
            Console.WriteLine("Now for words with <= 5 letters:");
            maxLength = 5;
            shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

我們只是將maxLength值改變了下,再次進行打印,演示結果如下:

C#3.0之delegate

為了更好的演示代碼,我們利用C#3.0中lamda表達式來進行演示,我們繼續接着上述來講,當我們將maxLength修改為5時,此時過濾的數據和4一樣,此時我們利用匿名方法或者lamda表達式同樣進行如上演示,如下。

            Console.Write("Maximum length of string to include? ");
            int maxLength = int.Parse(Console.ReadLine());

            Predicate<string> predicate = item => item.Length <= maxLength;
            IList<string> shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

            Console.WriteLine("Now for words with <= 5 letters:");
            maxLength = 5;
            shortWords = ListUtil.Filter(SampleData.Words, predicate);
            ListUtil.Dump(shortWords);

看看演示結果:

從上述演示結果可以看出此時的maxLength為5,當然打印過濾的結果則不一樣,這個時候就得說到第一個話題【變量捕獲】。在C# 2.0和3.0中的匿名方法和lambda表達式都能捕獲到本地變量。

那么問題來了,什么是變量捕獲呢?我們怎么去理解呢?

我們接下來利用lambda表達式再來看一個例子:

            var name = "cnblogs";
            Func<String> capture = () => name;
            name = "xpy0928";
            Print(capture);
        static void Print(Func<string> capture)
        {
            Console.WriteLine(capture());
            Console.ReadKey();
        }

那么打印的結果將會是什么呢?cnblogs?xpy0928?

name被捕獲,當本地變量發生改變時lambda也同樣作出對應的改變(因lambda會延遲執行),所以會輸出xpy0928。那么編譯器到底做了什么才使得輸出xpy0928呢?編譯器內部將上述代碼進行了大致如下轉換。

    public class Capture
    {
        public string name;
        public string printName()
        {
            return this.name;
        }
    }
    var capture = new Capture();
    capture.name = "cnblogs";
    capture.name = "xpy0928";
    Print(capture.printName);

到了這里想必我們能夠理解了捕獲變量的意義lambda始終指向當前對象中的name值即對象中的引用始終存在於lamda中。

接下來就要說到閉包,在此之前一直在討論變量捕獲,因為閉包產生的源頭就是變量捕獲。(個人理解,若有錯誤請指正)。

變量捕獲的結果就是編譯器將產生一個對象並將該局部變量提升為實例變量從而達到延長局部變量的生命周期,存儲到的這個對象叫做所謂的閉包。

上述這句話又是什么意思?我們再來看一個例子:

            List<Func<int>> funcs = new List<Func<int>>();

            for (int j = 0; j < 10; j++)

                funcs.Add(() => j);

            foreach (Func<int> func in funcs)

                Console.WriteLine(func());
            Console.ReadKey();

有人說上述例子就是閉包,對,是閉包且結果返回10個10,恩完事!不能就這樣吧,我們還得解釋清楚。我們一句一句來看。

  funcs.Add(() => j);

()=>j代表什么意思,來我們來看看之前lambda表示式的六部進化曲:

實例化一個匿名委托並返回j值,注意這里說()=>j是返回變量j的當前值而非返回值j。返回的匿名委托為一個匿名類並訪問此類中的屬性j(為什么說返回的匿名委托為一個匿名類,請看此鏈接:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html) 。好說完這里,我們再來解釋為什么打印10個10?

此時創建的每個匿名委托都捕獲了這個變量j,所以每個匿名委托即匿名類保持了對字段j的引用,當for循環完畢時即10時此時字段值全變為10,直到j不被匿名委托所引用,j才會被垃圾回收器回收。

我們再來看看對上述進行修改:

            List<Func<int>> funcs = new List<Func<int>>();

            for (int j = 0; j < 10; j++)
            {

                int tempJ = j;

                funcs.Add(() => tempJ);

            }
            foreach (Func<int> func in funcs)
                Console.WriteLine(func());
            Console.ReadKey();

很明顯將輸出0-9因為此時創建了一個臨時變量tempJ,當每次進行迭代時匿名委托即lambda捕獲的是不同的tempJ,所以此時能按照我們預期所輸出。

我們再來看一種情況:

             for (int j = 0; j < 10; j++)
            {
                Func<int> fun = () => j;
                Console.WriteLine(fun());

            }

此時還是正常輸出0-9,因為此時lambda表達式每次迭代完立即執行,而不像第一個例子直到延遲到循環到10才開始進行所有的lambda執行。

總結 

閉包概念:閉包允許你將一些行為封裝,將它像一個對象一樣傳來遞去,而且它依然能夠訪問到原來第一次聲明時的上下文。這樣可以使控制結構、邏輯操作等從調用細節中分離出來。

作用:(1)利於代碼精簡。(2)利於函數編程。(3)代碼安全。

參考資料:

C# in Depth:http://csharpindepth.com/Articles/Chapter5/Closures.aspx

Variable Capture in C# with Anonymous Delegates:http://www.digitallycreated.net/Blog/34/variable-capture-in-c%23-with-anonymous-delegates

Understanding Variable Capturing in C#:https://blogs.msdn.microsoft.com/matt/2008/03/01/understanding-variable-capturing-in-c/

C#與閉包:http://www.cnblogs.com/jujusharp/archive/2011/08/04/C-Sharp-And-Closure.html 

【溫馨提示】:無法看清演示?上述圖片現已添加點擊放大查看功能。


免責聲明!

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



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