【.NET深呼吸】清理对象引用,有一个问题容易被忽略

作者: 杭谨

更新时间:2022-03-26 13:11:43

2220 阅读

大家知道,托管代码一个重要的特点是自动管理内存,即我们常说的垃圾回收机制,那些高大上的理论我就不重复了,有兴趣的朋友可以翻书。我这个有个毛病——不喜欢很严肃地去说一些理论的东西,所以我不多介绍了。

一般而言,当代码执行超出某个变量的有效范围后,或者不再引用某个对象实例时,该实例会发生析构,垃圾回收器很可能就要清理门户了,当然也可能不是马上清理,也许会过一会儿再清理。

对于一些要自定义进行清理操作的类,我们会采取以下方案:

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;
        }

 

这个例子研究,告诉我们:在类实例中处理静态事件时一定要小心

本示例的源码下载:https://files.cnblogs.com/files/tcjiaan/refsample.zip

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

 

版权声明:本文著作权归作者【杭谨 】所有,不代表本网站立场。

侵权请联系:root_email@163.com