一本好書,或是一本比較有深度的書,就是每次研讀的時候都會有新的發現。
好吧,我承認每次讀的時候都有泛泛而過的嫌疑~~
這幾年一直專注于C#客戶端的開發,逐步從迷迷糊糊,到一知半解,再到自以為是,最后沉下心重新審視。也許這也是一種進步一種自我學習的過程。
前面啰嗦了這么多,希望大家也能不那么浮躁的“深入理解”C#這門語言的每個知識點。本文總結書本中的知識,在結合實際應用場合進行概述,如果有不正確的地方,還請不吝指教。
文章中的內容比較淺顯,請高手略過此文。
程序閉包的問題是由于程序對某些變量進行了預判和處理(個人理解,若有誤或不足請指正)使得某些變量理應作為值類型卻變為了引用類型導致數據異常。
當然,大多數情況下,我們是不會遇到這樣的問題,但在某些情況下,我們不得不注意并分析問題的根本原因,BUG永遠不是隨機的。
下面通過幾個例子逐步來理解閉包的概念:
例4.1
PRivate void Button1_Click(object sender, RoutedEventArgs e) { int outerVariableCaptured = 5; // 外部變量(被捕獲) int outerVariableUnCaptured = 50; // 外部變量(未捕獲) if (DateTime.Now.Hour <= 24) { int normalLocalVariable = 1; // 普通方法的局部變量,不是外部變量,因為在其作用域內無匿名方法。 this.Txb_Msg.Text += string.Format("普通方法的局部變量 = {0}", normalLocalVariable) + System.Environment.NewLine; } Action x = new Action(() => { int anonLocal = 2; // 匿名方法的局部變量 this.Txb_Msg.Text += string.Format("匿名方法的局部變量 = {0}", anonLocal) + System.Environment.NewLine; this.Txb_Msg.Text += string.Format("匿名方法中被捕獲的外部變量 = {0}", outerVariableCaptured) + System.Environment.NewLine; // 匿名方法中調用了作用域外的變量,所以變量變為被捕獲的外部變量 }); this.Txb_Msg.Text += string.Format("普通方法的未捕獲的外部變量 = {0}", outerVariableUnCaptured) + System.Environment.NewLine; x(); }
輸出結果:普通方法的局部變量 = 1普通方法的未捕獲的外部變量 = 50匿名方法的局部變量 = 2匿名方法中被捕獲的外部變量 = 5
4.1中是讓大家了解外部變量,局部變量,捕獲等相關概念。其中最重要的是被捕獲的外部變量。
例4.2
private void Button3_Click(object sender, RoutedEventArgs e) { // 證明被捕捉的局部變量聲明周期被延長了。 OnCreateDelegate += MainWindow_OnCreateDelegate; this.Dispatcher.Invoke(OnCreateDelegate(this)); // 此處Invoke容易引起歧義:原因在于Invoke事件之后返回的還是一個事件。 // Counter是值類型,逃脫其作用域時棧上數據會被回收,真實的情況是這樣嗎? // 從另一個側面也說明了,值類型是在棧上還是堆上,依賴于創建對象的類型。 }
private Delegate MainWindow_OnCreateDelegate(object sender) { var frm = sender as MainWindow; int Counter = 1; var a = new Action(() => { frm.Txb_Msg.Text += string.Format("委托內部的變量值 = {0}", Counter) + System.Environment.NewLine; Counter++; }); a(); return a; }
輸出:委托內部的變量值 = 1委托內部的變量值 = 2
這個例子要說明的是Counter其值類型原本的生存周期應該在MainWindow_OnCreateDelegate(object sender)方法中,可是偏偏卻逃離了方法的作用域,這就是我們所說的值類型是在堆上Or棧上完全取決于其初始化的位置是在棧上還是在堆上。
例4.3
private void Button4_Click(object sender, RoutedEventArgs e) { // 更復雜的一些情況 var methods = new Action[2]; int outside = 10; // 實例化變量一次 for (int i = 0; i < 2; i++) { int inside = 100; // 實例化變量多次 methods[i] = new Action(() => { this.Txb_Msg.Text += string.Format("Inside Value = {0}; Outside Value = {1} ", inside, outside) + System.Environment.NewLine; inside++; outside++; // 匿名方法捕獲的變量 }); } methods[0].Invoke(); methods[0].Invoke(); methods[0].Invoke(); methods[1].Invoke(); }
輸出結果: /***************Outside變量內存共享************* * Inside Value = 100; Outside Value = 10 * Inside Value = 101; Outside Value = 11 * Inside Value = 102; Outside Value = 12 * Inside Value = 100; Outside Value = 13 * *******************************************/
這個例子就得好好想想了,outside和inside的值到底會是什么?為什么會這樣?原因在于inside在For循環的內部初始化了多次,也就是說For循環幾次,就有幾個獨立的inside對象,雖說它是值類型。
例4.4
這個例子摘自《編寫高質量代碼:改善C#程序的157個建議》
private void Button5_Click(object sender, RoutedEventArgs e) { // 閉包陷阱 var methods = new Action[2]; for (int i = 0; i < 2; i++) { int inside = i; // 實例化變量多次 methods[i] = new Action(() => { this.Txb_Msg.Text += string.Format("Inside Value = {0}; Index Value = {1} ", inside, i) + System.Environment.NewLine; }); } methods[0].Invoke(); methods[1].Invoke(); /*****************閉包陷阱********************* * 當使用i的值時,i就是前面說的共享變量(捕獲的外部變量),所以總是輸出i的最大值。 * 當使用Inside值時,Inside就是內部變量,每次創建對象都重新生成,所以此處inside的值是遞增,即緩存下i的值。 * 對于IL,其創建了Tempclass.i來代替i,導致了i值共享。 * *******************************************/ }
輸出結果:Inside Value = 0; Index Value = 2 Inside Value = 1; Index Value = 2
其實如果用ILDasm來看的話,針對i這個對象,IL生成了一個DisplayClass(就是一個名字而已)這樣一個類,最總導致了,i變為引用類型,數據異常。
持續更新:示例代碼下載
新聞熱點
疑難解答