当前位置:首页 > 技能相关 > C#与C++ > 正文内容

c#高级编程 处理非托管的资源

admin2年前 (2023-01-30)C#与C++2660 修订时间:2023-01-30 13:48:40

垃圾回收器的出现意味着,通常不需要担心不再需要的对象,只要让这些对象的所有引用都超出作用域,并允许垃圾回收器在需要时释放内存即可。但是,垃圾回收器不知道如何释放非托管的资源(例如,文件句柄、网络连接和数据库连接)。托管类在封装对非托管资源的直接或间接引用时,需要制定专门的规则,确保非托管的资源在回收类的一个实例时释放。

在定义一个类时,可以使用两种机制来自动释放非托管的资源。这些机制常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。这两种机制是:

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

● 在类中实现System.IDisposable接口

下面依次讨论这两种机制,然后介绍如何同时实现它们,以获得最佳的效果。

1、析构函数或终结器

了构造函数可以指定必须在创建类的实例时进行的某些操作。相反,在垃圾回收器销毁对象之前,也可以调用析构函数。由于执行这个操作,因此析构函数初看起来似乎是放置释放非托管资源、执行一般清理操作的代码的最佳地方。但是,事情并不是如此简单。

注意: 在讨论C#中的析构函数时,在底层的.NET体系结构中,这些函数称为终结器(finalizer)。在C#中定义析构函数时,编译器发送给程序集的实际上是Finalize()方法。它不会影响源代码,但如果需要查看生成的IL代码,就应知道这个事实。

C++开发人员应很熟悉析构函数的语法。它看起来类似于一个方法,与包含的类同名,但有一个前缀波形符(~)。它没有返回类型,不带参数,没有访问修饰符。下面是一个例子:

class MyClass
{
~MyClass()
{
// Finalizer implementation
}
}

C#编译器在编译析构函数时,它会隐式地把析构函数的代码编译为等价于重写Finalize()方法的代码,从而确保执行父类的Finalize()方法。下面列出的C#代码等价于编译器为~

MyClass()析构函数生成的IL:
protected override void Finalize()
{
try
{
// Finalizer implementation
}
finally
{
base.Finalize();
}
}

如上所示,在~MyClass()析构函数中实现的代码封装在Finalize()方法的一个try块中。对父类的Finalize()方法的调用放在finally块中,确保该调用的执行。

C#析构函数要比C++析构函数的使用少得多。与C++析构函数相比,C#析构函数的问题是它们的不确定性。在销毁C++对象时,其析构函数会立即运行。但由于使用C#时垃圾回收器的工作方式,无法确定C#对象的析构函数何时执行。所以,不能在析构函数中放置需要在某一时刻运行的代码,也不应寄望于析构函数会以特定顺序对不同类的实例调用。如果对象占用了宝贵而重要的资源,应尽快释放这些资源,此时就不能等待垃圾回收器来释放了。

是C#析构函数的实现会延迟对象最终从内存中删除的时间。没有析构函数的对象会在垃圾回收器的一次处理中从内存中删除,但有析构函数的对象需要两次处理才能销毁:第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。另外,运行库使用一个线程来执行所有对象的Finalize()方法。如果频繁使用析构函数,而且使用它们执行长时间的清理任务,对性能的影响就会非常显著。

2、IDisposable接口

在C#中,推荐使用System.IDisposable接口替代析构函数。IDisposable接口定义了一种模式(具有语言级的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。MyClass类的Dispose()方法的实现代码如下:

class MyClass: IDisposable
{
public void Dispose()
{
// implementation
}
}

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

假定有一个ResourceGobbler类,它需要使用某些外部资源,且实现IDisposable接口。如果要实例化这个类的实例,使用它,然后释放它,就可以使用下面的代码:

var theInstance = new ResourceGobbler();
// 做你的处理
theInstance.Dispose();

但是,如果在处理过程中出现异常,这段代码就没有释放theInstance使用的资源,所以应使用try块,编写下面的代码:

ResourceGobbler theInstance = null;
try
{
theInstance = new ResourceGobbler();
// do your processing
}
finally
{
theInstance? .Dispose();
}

3、using语句

使用try/finally,即使在处理过程中出现了异常,也可以确保总是在theInstance上调用Dispose()方法,总是释放theInstance使用的任意资源。但是,如果总是要重复这样的结构,代码就很容易被混淆。C#提供了一种语法,可以确保在实现IDisposable接口的对象的引用超出作用域时,在该对象上自动调用Dispose()方法。该语法使用了using关键字来完成此工作——该关键字在完全不同的环境下,它与名称空间没有关系。下面的代码生成与try块等价的IL代码:

using (var theInstance = new ResourceGobbler())
{
// do your processing
}

using语句的后面是一对圆括号,其中是引用变量的声明和实例化,该语句使变量的作用域限定在随后的语句块中。另外,在变量超出作用域时,即使出现异常,也会自动调用其Dispose()方法。

注意: using关键字在C#中有多个用法。using声明用于导入名称空间。using语句处理实现IDisposable的对象,并在作用域的末尾调用Dispose方法。

4、实现IDisposable接口和析构函数

上文讨论了自定义类所使用的释放非托管资源的两种方式:

● 利用运行库强制执行的析构函数,但析构函数的执行是不确定的,而且,由于垃圾回收器的工作方式,它会给运行库增加不可接受的系统开销。

● IDisposable接口提供了一种机制,该机制允许类的用户控制释放资源的时间,但需要确保调用Dispose()方法。

如果创建了终结器,就应该实现IDisposable接口。假定大多数程序员都能正确调用Dispose()方法,同时把实现析构函数作为一种安全机制,以防没有调用Dispose()方法。下面是一个双重实现的例子:

using System;
public class ResourceHolder : IDisposable
{
    private bool _isDisposed = false;
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (!_isDisposed)
        {
            if (disposing)
            {
                // Cleanup managed objects by calling their
                // Dispose() methods.
            }
            // Cleanup unmanaged objects
        }
        _isDisposed = true;
    }
~ResourceHolder()
    {
        Dispose(false);
    }
    public void SomeMethod()
    {
        // 确保在执行任何方法之前尚未释放对象
        if (_isDisposed)
        {
            throw new ObjectDisposedException("ResourceHolder");
        }//方法实现…
    }
}

从上述代码可以看出,Dispose()方法有第二个protected重载方法,它带一个布尔参数,这是真正完成清理工作的方法。Dispose(bool)方法由析构函数和IDisposable.Dispose()方法调用。这种方式的重点是确保所有的清理代码都放在一个地方。

传递给Dispose(bool)方法的参数表示Dispose(bool)方法是由析构函数调用,还是由IDisposable.Dispose()方法调用——Dispose(bool)方法不应从代码的其他地方调用,其原因是:

● 如果使用者调用IDisposable.Dispose()方法,该使用者就指定应清理所有与该对象相关的资源,包括托管和非托管的资源。

● 如果调用了析构函数,原则上所有的资源仍需要清理。但是在这种情况下,析构函数必须由垃圾回收器调用,而且用户不应试图访问其他托管的对象,因为我们不再能确定它们的状态了。在这种情况下,最好清理已知的非托管资源,希望任何引用的托管对象还有析构函数,这些析构函数执行自己的清理过程。

_isDisposed成员变量表示对象是否已被清理,并确保不试图多次清理成员变量。它还允许在执行实例方法之前测试对象是否已清理,如SomeMethod()方法所示。这个简单的方法不是线程安全的,需要调用者确保在同一时刻只有一个线程调用方法。要求使用者进行同步是一个合理的假定,在整个.NET类库中(例如,在Collection类中)反复使用了这个假定。

最后,IDisposable.Dispose()方法包含一个对System.GC.SuppressFinalize()方法的调用。GC类表示垃圾回收器,SuppressFinalize()方法则告诉垃圾回收器有一个类不再需要调用其析构函数了。因为Dispose()方法已经完成了所有需要的清理工作,所以析构函数不需要做任何工作。调用SuppressFinalize()方法就意味着垃圾回收器认为这个对象根本没有析构函数。






 您阅读本篇文章共花了: 

免责声明
本站内容均为博客主本人日常使用记录的存档,如侵犯你的权益请联系:lifei@zaiheze.com 546262132@qq.com 沟通删除事宜。本站仅带访问端口形式使用,已杜绝搜索引擎爬取。

扫描二维码推送至手机访问。

版权声明:本文由LIFEI - blog发布,如需转载请注明出处。

本文链接:http://www.lifeiai.com/?id=289

分享给朋友:

相关文章

Web API的创建3年前 (2022-11-07)
C# 第一篇 踏上征程 3年前 (2022-11-14)
C# 第二篇 基础语法3年前 (2022-11-14)
C# 第三篇 流程控制3年前 (2022-11-15)

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。