【.NET深呼吸】清理對象引用,有一個問題容易被忽略


大家知道,托管代碼一個重要的特點是自動管理內存,即我們常說的垃圾回收機制,那些高大上的理論我就不重復了,有興趣的朋友可以翻書。我這個有個毛病——不喜歡很嚴肅地去說一些理論的東西,所以我不多介紹了。

一般而言,當代碼執行超出某個變量的有效范圍后,或者不再引用某個對象實例時,該實例會發生析構,垃圾回收器很可能就要清理門戶了,當然也可能不是馬上清理,也許會過一會兒再清理。

對於一些要自定義進行清理操作的類,我們會采取以下方案:

1、寫上析構函數,在析構函數中清理。

2、實現IDisposable接口,並實現Dispose方法,在方法中編寫自定義清理代碼。當該類型被實例化后,最后不再使用時會調用Dispose方法清理,如果順利清理,最后還會調用類型的析構函數。通常,如何實現了IDisposable接口,就不必再寫上析構函數了。如果希望Dispose方法被自動調用,可以在實例化對象的代碼包裝在using語句塊中,當執行完using塊時會自動調用Dispose方法。

 

可能有人笑了,老周,你太逗了,這些基礎知識誰不知道?當然,我說上面那些內容是為了繞個小圈子,以便進入主題。於是,我產生了一個疑問:是不是存在某些情景下,可能導致對象實例不會被回收呢?就算你調用了Dispose方法,就算你把變量設為null來解除引用,就算你調用GC類的方法來回收……

經過老周測試,還真有這種情況,而且很多朋友都很有可能會忽略,甚至在意識認知上誤認為對象實例已經被回收,而實際上是沒有回收的。

 

我簡單說一下這種情形:

比如有一個靜態類(靜態類的成員必是靜態的)A,里面有靜態事件。隨后在其他類的實例中處理A類的靜態事件,並且處理事件的方法就位於這個實例對象上……

不急,我們還是看真實的例子吧。假如我定義了一個靜態類MyChecker,它里面有個靜態事件CheckEvent。

    public static class MyChecker
    {
        #region 靜態事件
        public static event EventHandler CheckEvent;
        #endregion

        public static void CallEvent()
        {
            if (CheckEvent != null)
            {
                CheckEvent(new object(), EventArgs.Empty);
            }
        }
    }

只要CallEvent方法被調用,CheckEvent事件會被引發。

 

然后,定義另一個類SampleClass,並在該類中處理剛才MyChecker中的靜態事件。

    class SampleClass:IDisposable
    {
        public SampleClass()
        {
            MyChecker.CheckEvent += MyChecker_CheckEvent;
        }

        void MyChecker_CheckEvent(object sender, EventArgs e)
        {
            new Form2().Show();
        }

        ~SampleClass()
        {
            System.Diagnostics.Debug.WriteLine("\n看,析構函數調用了。\n");
        }

        public void Dispose()
        {
            //……
        }
    }


在類的構造函數中,附加CheckEvent事件的處理,處理方法名為MyChecker_CheckEvent。

可能大家已經發現,老周寫的SampleClass類有點恐怖氣息,既實現了Dispose方法,怎么又寫了析構函數,我這里寫上析構函數是為了驗證類的實例是否真的被清理,如果實例真的被回收,那么Debug類會在“輸出”窗口中輸出提示,如果沒有提示輸出,說明類的實例還霸占着內存。

接下來測試一下。

            SampleClass sc = new SampleClass();
            await Task.Delay(10 * 1000);
            sc.Dispose();
            sc = null;
            GC.Collect();


實例化SampleClass后,然后Delay會暫停10秒,10秒鍾過后會調用Dispose方法,並設置變量為null引用,我害怕不能及時清理,連GC.Collect方法也用上了。

而在等待這10秒期間,可以調用靜態類的CallEvent方法來引發靜態事件CheckEvent。

MyChecker.CallEvent();

按照一般理解,在10秒鍾后,SampleClass實例應該被清理,並且在“輸出”窗口會輸出提示。

好,現在試一下。

……

實驗結果表明,輸出 窗口中連鴨毛都沒有輸出,這說明10秒鍾后,SampleClass實例根本沒有發生析構。於是又出問題了,這是怎么回事?SampleClass實例不是不存在引用了嗎,怎么不發生析構?

其實我們忽略了一點:靜態事件CheckEvent還跟SampleClass實例的方法綁定着呢,實質上,雖然將變量設為null,可是SampleClass實例中的MyChecker_CheckEvent方法還被靜態類中的靜態事件引用着,所以不會被回收。不知道你明白了沒。

這個問題很多朋友在實際開發中都會忽略,還得意地以為Sample實例真的被回收了,實際上實例不會被回收,除非程序結束。因為MyChecker是靜態類,不基於實例。如果MyChecker不是靜態類,那么當MyChecker的實例釋放后,SampleClass實例就可以被釋放了。

那么,如何解決呢?很簡單,只要在SampleClass類的Dispose方法中解除靜態事件與方法的綁定即可,這樣的話,靜態事件就不再引用實例中的方法成員了,此時實例就可以發生析構了。

        public void Dispose()
        {
            MyChecker.CheckEvent -= MyChecker_CheckEvent;
        }

 

這個例子研究,告訴我們:在類實例中處理靜態事件時一定要小心

本示例的源碼下載:http://files.cnblogs.com/files/tcjiaan/refsample.zip

好了,今天就扯到這里吧。

 


免責聲明!

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



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