CLR内存管理之释放非托管资源

本文深入探讨了C#中非托管资源的管理方法,对比了析构函数和IDisposable接口的特点与使用场景,详细解释了如何在类中实现这两种机制来自动释放文件句柄、网络连接等资源,同时讨论了析构函数的不确定性和性能影响,以及IDisposable接口提供的确定性机制,最后展示了如何综合使用这两种方法进行资源释放。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇文章中我提到,CLR通过栈指针给变量分配内存空间,通过GC来释放不再引用的内存空间。GC虽然减少了程序员处理内存的困难,但它也有局限性,它不能处理像文件句柄、网络连接、数据库连接这样的非托管资源。在定义一个类时,我们使用两种机制来自动释放非托管资源:

1,声明一个析构函数(或终结器finalizer),作为类的一个成员

2,在类中实现System.IDisposable接口

析构函数:

析构函数的声明格式如下

复制代码
class Hotel
{
       ~Hotel()
        {
            //destructor implementation
        }

}
复制代码

在GC销毁对象之前,会调用对象的析构函数。C#编译器在编译析构函数时,会隐式地把析构函数的代码编译为等价于Finalize()方法的代码,从而确保执行父类的Finalize()方法。下面模拟了编译器编译析构函数生成的c#代码:

复制代码
protected override void Finalize()
{
     try
      {
         //destructor implementation
      }
      finally
      {
           base.Finalize();
      }
}
复制代码

如上所示,在~Hotel()析构函数中的实现代码封装在Finalize()方法的一个try语句块中。对父类的Finalize()方法的调用放在finally块中,确保该调用的执行。
有经验的C++开发人员会大量使用析构函数,用于清理资源,提供调试信息或执行一些其他的操作。这是因为,C++中,销毁对象时,它的析构函数会立即执行。但是,C#中,我们使用析构函数的次数却少之又少。C#析构函数的问题在于它的不确定性,由于GC的工作方式,我们无法确定C#析构函数何时会执行。因此,我们不敢在析构函数中放置一些需要在特定时刻执行的代码。对象如果占用了宝贵而重要的资源,应该尽快释放这些资源,此时就不能指望GC来自动清理资源了。另一个问题是,C#析构函数的实现会延迟对象从内存中删除的时间。没有析构函数的对象会在GC的一次处理中从内存中删除。但有析构函数的对象却需要两次的处理才能销毁。第一次调用不会删除,第二次调用时才会执行删除操作。另外,CLR使用一个线程来执行所有对象的Finalize()方法,如果频繁使用析构函数,并让它们执行长时间的清理任务,对性能的影响就会非常显著。

实现IDisposable接口:

在C#中,推荐使用System.IDisposable接口代替析构函数。IDisposable接口为释放非托管资源提供了一种确定的机制,并避免产生析构函数固有的与GC相关的问题。IDisposable接口的实现如下:

复制代码
class Hotel: IDisposable
 {
        public void Dispose()
        {
            //implementation            
        }
}
复制代码

Dispose()方法的实现代码显式地释放由对象直接使用的非托管资源,并在所有也实现IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法就为释放非托管资源提供了一种精确的控制。
看下面的代码:

Hotel hotel = new Hotel();
hotel.Dispose();

该代码会调用hotel对象的Dispose()方法,但是如果执行过程出现异常,这段代码就不会释放对象使用的资源。所以,我们对它加以改进:

复制代码
            Hotel hotel = null;
            try
            {
                hotel = new Hotel();
            }
            finally
            {
                if (hotel != null)
                {
                    hotel.Dispose();
                }
            }
复制代码

这样就能确保,即使执行过程出现异常,也总能保证在hotel上调用Dispose()方法。但是如果我们的项目中重复使用这样的结构,代码就会变得非常杂乱。C#提供了一种语法,使用using关键字,生成与上面等同的代码:

           using (Hotel hotel = new Hotel())
            {
                //operation
            }

是不是很熟悉,我们在使用数据库连接和网络连接时经常用到这种写法。它也可以保证在出现异常时调用Dispose()方法。
在实际应用中,我们需要综合使用以上两种方法来进行非托管资源的释放工作:

class Hotel: IDisposable
   {
       private bool isDisposed = false;
 
       ~Hotel()
       {
           Dispose(false);
       }
 
       public void Dispose()
       {
           Dispose(true);
           GC.SuppressFinalize(this);
       }
 
       protected virtual void Dispose(bool disposting)
       {
           if (!isDisposed)
           {
               if (disposting)
               {
                   //Cleanup managed objects by calling their Dispose()
               }
               //Cleanup unmanaged objects
           }
           isDisposed = true;
       }
       public void Operate()
       {
           if (isDisposed)
           {
               throw new ObjectDisposedException("Hotel");
           }
           //implementation
       }       
   }

在该示例中,Dispose(bool disposting)方法执行真正的清理工作。它接受一个参数disposting,用来标示调用是来自Dispose()方法还是析构函数。在调用Dispose()方法时,我们用到了GC.SuppressFinalize(this);它会告诉GC,在调用当前对象的Dispose()方法时,不再执行对象的析构函数。在Operate()方法中,会根据变量isDisposed来确定对象是否已经被销毁,如果被销毁,会抛出一个ObjectDisposedException类型的异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值