c#对于如何释放资源的解释

本文探讨了在C#中如何有效地管理非托管资源,包括使用Dispose()方法、Dispose模式及using语句的重要性。文章详细解释了Dispose()与Close()的区别,并提供了使用using语句的正确方式。

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

 

当我们使用非托管资源(unmanaged resources)类型时,应当使用IDisposable接口的Dispose()方法来释放资源。在.Net环境中,对非托管资源的回收不是系统的责任,我们必须自己调用Dispose()方法来释放资源。确保非托管资源会释放的最好方法是使用using或者try/finally。

      所有的非托管资源类型都实现了IDisposable接口。另外当我们没有明确的释放资源,比如我们忘记了,C#还会防护性的通过创建终结器(finalizer)来释放资源。如果我们希望应用程序运行的更快时,就应当尽快释放一些不必要的资源。幸运的是在C#中有新的关键字来完成这项任务。我们先考虑下面的代码:

        public void ExcuteCommand(string connectString, string commandString)
        {
            SqlConnection myConnection = new SqlConnection(connectString);
            SqlCommand myCommand = new SqlCommand(commandString, myConnection);
            myConnection.Open();
            myCommand.ExecuteNonQuery();
        }
      有两个对象没有被释放掉:SqlConnection和SqlCommand。它们都会保存在内存中直到终结器被调用为止。

      通过下面的修改,我们可以释放它们:

        public void ExcuteCommand(string connectString, string commandString)
        {
            SqlConnection myConnection = new SqlConnection(connectString);
            SqlCommand myCommand = new SqlCommand(commandString, myConnection);
            myConnection.Open();
            myCommand.ExecuteNonQuery();

            myCommand.Dispose();
            myConnection.Dispose();
        }
 

      这样做是正确的,但前提是SqlCommand没有抛出异常。一旦出现异常,我们的Dispose()方法就不会运行了。using关键字可以帮助我们确保Dispose()会被运行。当我们使用using的时候,C#的编译器会将它转换成为类似与try/finally的形式:

        public void ExcuteCommand(string connectString, string commandString)
        {
            using(SqlConnection myConnection = new SqlConnection(connectString))
            {
                using(SqlCommand myCommand = new SqlCommand(commandString, myConnection))
                {
                    myConnection.Open();
                    myCommand.ExecuteNonQuery();
                }
            }
        }
 

      下例中的两段代码会生成非常相似的IL

using(SqlConnection myConnection = new SqlConnection(connectString))
{
      myConnection.Open();
}

try
{
      SqlConnection myConnection = new SqlConnection(connectString);
      myConnection.Open();
}
finally
{
      myConnection.Dispose();
}
 

      当我们使用非托管资源时,使用using是确保资源合理释放的简单途径。如果我们对不支持IDisposable接口的类型使用using关键字,编译器会报错:

//错误
using(string msg = "this is a message")
{
      Console.WriteLine(msg);
}
      另外using只检验编译时类型是否支持IDisposable接口,它不能识别运行时的对象。下例中即便Factory.CreateResource()返回的类型支持IDisposable接口也是不能通过编译的:

//错误
using(object obj = Factory.CreateResource)
{
}
 

      对于可能支持可能不支持IDisposable接口的对象,我们可以这样来处理:

object obj = Factory.CreateResource();
using(obj as IDisposable)
{
}
 

      如果对象实现了IDisposable接口,就可以生成释放资源的代码。如果不支持,则生成using(null),虽然不做任何工作,但也是安全的。如果我们拿不准是否应该将对象放在using中,那么比较稳妥的做法是将它放进去。

      当我们在程序中使用了非托管资源类型时,我们应当将其放入using的括号中。当有多个需要释放的资源,例如前面的例子中的connection和command,我们应当创建多个using,每一个包含一个对应的对象。这些using会被转化为不同的try/finally块,在效果上看就好像是下面这段代码:

        public void ExcuteCommand(string connectString, string commandString)
        {
            SqlConnection myConnection = null;
            SqlCommand myCommand = null;
            try
            {
                myConnection = new SqlConnection(connectString);
                try
                {
                    myCommand = new SqlCommand(commandString, myConnection);
                    myConnection.Open();
                    myCommand.ExecuteNonQuery();
                }
                finally
                {
                    if(myCommand != null)
                    {
                        myCommand.Dispose();
                    }
                }
            }
            finally
            {
                if(myConnection != null)
                {
                    myConnection.Dispose();
                }
            }
        }
      每个using声明都创建了一个try/finally程序块。我们自己也可以通过这样写来取消多层嵌套:

        public void ExcuteCommand(string connectString, string commandString)
        {
            SqlConnection myConnection = null;
            SqlCommand myCommand = null;
            try
            {
                myConnection = new SqlConnection(connectString);
                myCommand = new SqlCommand(commandString, myConnection);

                myConnection.Open();
                myCommand.ExecuteNonQuery();
            }
            finally
            {
                if(myCommand != null)
                {
                    myCommand.Dispose();
                }
                if(myConnection != null)
                {
                    myConnection.Dispose();
                }
            }
        }
 

      虽然看起来很简洁,但是我们不要这样使用using声明:

        public void ExcuteCommand(string connectString, string commandString)
        {
            SqlConnection myConnection = new SqlConnection(connectString);
            SqlCommand myCommand = new SqlCommand(commandString, myConnection);
            using(myConnection as IDisposable)
            {
                using(myCommand as IDisposable)
                {
                    myConnection.Open();
                    myCommand.ExecuteNonQuery();
                }
            }
        }
 

      这样做是有潜在bug的。一旦SqlCommand的构造函数抛出异常,SqlConnection就无法被释放了。我们必须保证每个非托管资源对象都可以被顺利的释放,否则可能会造成内存资源的浪费。对于单个的非托管资源对象,使用using关键字是最好的方法。对于多个对象,我们可以使用嵌套using或者自己写try/finally的方法来释放资源。

      在释放资源上还有一个细节,有些类型不仅有Dispose()方法,还有Close()方法,例如SqlConnection。我们这样可以关闭SqlConnection的连接:

myConnection.Close();
      这样做的确能够释放连接,但是并不是和Dispose()方法一样。Dispose()方法除了释放资源之外,还有其他的工作:它会通知垃圾收集器(Garbage Collector)这个对象的资源已经被释放了,而不必在终结器中进行重复的操作。Dispose()调用了GC.SuppressFinalize()方法,这个方法请求系统不要调用指定对象的终结器。而Close()不是这样的。使用Close()释放的对象虽然已经不必调用终结器,但是它还是存在于终结器的释放资源队列当中。Dispose()比Close()的工作做的更加彻底。

      Dispose()并没有将对象移出内存。它为对象添加了一个释放资源的钩子(hook)。这就意味着我们可以对正在使用的对象使用Dispose(),我们应当小心这一点。

      在C#中大部分的类型都不支持Dispose()。在超过1500种类型中只有100来种实现了IDispose接口。当我们使用实现了这个接口的对象时,我们应当在适当的时候使用using或try/finally块的方法来释放资源。

      译自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著

      回到目录

P.S. 以下为个人想法:

      Dispose()和Close()都是可以显示调用来释放资源。而Finalize()是受保护的,析构函数更是不知道什么时候会被执行。被Close()掉的对象是有可能再次复活的,比如SqlConnection,还可以通过Open()继续使用,好像是休眠状态。而被Dispose()掉的对象从理论上来说是不应当再复活了,因为我们在Dispose的同时应当告诉GC这个对象已经over了,以避免重复的对象清除工作。当然我们可以通过释放资源时获得其引用的诡异方法来继续访问对象,但是由于Dispose()调用GC.SuppressFinalize()方法免除终结,因此这些Dispose()掉又复活的对象的资源就再也不会自动被GC释放了。

    public class DisposeClass : IDisposable
    {
        private string insideResources;

        public void UseInsideResources()
        {
            insideResources = "use resouces";
            Console.WriteLine(insideResources);
        }

        public DisposeClass()
        {
            Console.WriteLine("create object");
        }

        ~DisposeClass()
        {
            Console.WriteLine("destroy~ object");
        }

        IDisposable 成员#region IDisposable 成员

        public void Dispose()
        {
            // TODO:  添加 DisposeClass.Dispose 实现
            Console.WriteLine("dispose object");
            GC.SuppressFinalize( this );
        }

        #endregion
    }
      上例中的类在使用Dispose()释放资源后是不会调用析构函数的,没有调用Dispose()时才会在需要终结时调用析构函数。但是如果我们这样使用它:

    static void Main(string[] args)
    {
        ArrayList myList = new ArrayList();
        using(DisposeClass a = new DisposeClass())
        {
            myList.Add(a);
        }
        ((DisposeClass)(myList[0])).UseInsideResources();
        Console.ReadLine();
    }
      已经被Dispose掉的对象又复活了。而且析构函数不会再被调用。我们同样可以在析构函数中做这种事,只是我们不知道它什么时候会发生而已。

      Finalize()是一个神奇的函数,在没有析构函数时它可以像诸如abc()一样做为普通的成员函数,但是一旦析构函数存在,就会报出“已经存在名为Finalize()成员”的错误。更匪夷所思的是当我们将其声明为保护成员函数(开始时我将其声明为public的,不会出现特殊的结果,后来才改成protected)时,它就会被其派生类的析构函数调用。在C#中,派生类的析构函数会自动调用基类的析构函数,因此在这里派生类看来已经把它当作是基类的析构函数了。在编译器中应该将它同析构函数划了等号了吧,可是object.Finalize()明明是不能重写的。对于protected void Finalize()来说,它到底算是个什么东西呢?这应该是在编译阶段就可以发现的错误吧?代码如下:

    static void Main(string[] args)
    {
        ExtendDisposeClass a = new ExtendDisposeClass();
        Console.ReadLine();
    }

    public class DisposeClass : IDisposable
    {
        public DisposeClass()
        {
            Console.WriteLine("create object");
        }

//        ~DisposeClass()
//        {
//            Console.WriteLine("destroy~ object");
//        }

        protected void Finalize()
        {
            Console.WriteLine("finalize object");
        }

        IDisposable 成员#region IDisposable 成员

        public void Dispose()
        {
            // TODO:  添加 DisposeClass.Dispose 实现
            Console.WriteLine("dispose object");
        }

        #endregion
    }

    public class ExtendDisposeClass : DisposeClass
    {
        ~ExtendDisposeClass()
        {
            Console.WriteLine("destroyExtendDisposeClass object");
        }
    }

<think>嗯,用户想知道如何在C#中手动释放资源的最佳实践和方法,特别是关于using语句和实现IDisposable接口。首先,我需要回忆一下C#资源管理的相关知识。记得在C#里,托管资源通常由垃圾回收器自动处理,但非托管资源,比如文件句柄、数据库连接这些,需要手动释放。这时候IDisposable接口就派上用场了。 首先,用户的问题提到了using语句和IDisposable接口,所以应该先解释这两个部分。IDisposable接口定义了Dispose()方法,用于释放资源。实现这个接口的类需要自己管理资源释放。然后,using语句可以自动调用Dispose()方法,确保资源被及时释放,即使在发生异常的情况下也是如此。需要结合引用的资料来支持这些点,比如引用[1]和引用[4]。 接下来,要分步骤说明最佳实践。第一步是实现IDisposable接口,这里可能需要详细说明如何正确实现,包括区分托管和非托管资源,以及处理多次释放的情况。引用[3]提到,当一个类实现了IDisposable接口,就表明它持有需要特殊处理的资源。第二步是使用using语句,这样代码更简洁,尤其是在C# 8.0以后,using声明可以更简化代码,如引用[2]所述。 然后,可能需要提到手动调用Dispose()的情况,虽然不常见,但有时候需要显式释放,比如在长时间运行的方法中。同时,注意避免在Dispose()中抛出异常,引用[1]和引用[3]可能都有相关内容。 另外,用户可能想知道为什么需要手动释放资源,这部分可以引用引用[1]中的内容,说明资源泄漏和性能问题。还有,需要提到Finalizer(终结器)的使用,确保即使没有调用Dispose(),资源也能在垃圾回收时被释放,但这样可能会影响性能,所以最好还是显式调用。 可能还需要比较using语句和try-finally块的区别,说明using的优势,比如代码简洁性和可读性。引用[4]中的例子使用了using语句,应该提到这个例子。 最后,生成相关问题的时候,要围绕IDisposable接口的实现细节、using语句的其他用途、与Finalizer的关系,以及资源泄漏的预防等方面提问。这样用户如果有进一步的问题,可以继续探索这些方向。 需要确保回答结构清晰,分步骤说明,每个步骤都结合引用资料中的内容,并正确标注引用标识。同时,语言要简洁明了,避免技术术语过多,让用户容易理解。</think>### C# 手动释放资源的最佳实践和方法 #### 1. **实现`IDisposable`接口** 在C#中,手动释放资源的核心是**实现`IDisposable`接口**,通过定义`Dispose()`方法显式释放资源。 - **适用场景**:需要释放非托管资源(如文件句柄、数据库连接)或托管资源(如事件订阅)时[^1][^3]。 - **实现要点**: - 区分托管资源和非托管资源,仅在`Dispose()`中释放非托管资源。 - 通过`disposed`标志防止重复释放。 - 提供终结器(Finalizer)作为安全网,确保资源最终被释放[^1][^3]。 ```csharp public class MyResource : IDisposable { private bool disposed = false; private IntPtr handle; // 非托管资源示例 // 实现Dispose方法 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // 避免重复调用终结器 } protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { // 释放托管资源(如其他IDisposable对象) } // 释放非托管资源 CloseHandle(handle); handle = IntPtr.Zero; disposed = true; } } ~MyResource() // 终结器 { Dispose(false); } } ``` #### 2. **使用`using`语句自动释放** `using`语句是**最简洁的自动释放方式**,确保资源在代码块结束时释放,即使发生异常[^2][^4]。 - **语法示例**: ```csharp using (var resource = new MyResource()) { // 使用resource对象 } // 自动调用resource.Dispose() ``` - **C# 8.0优化**:支持更简洁的`using`声明: ```csharp using var resource = new MyResource(); // 作用域结束时自动释放 ``` #### 3. **手动调用`Dispose()`** 在无法使用`using`时(如长生命周期对象),需**显式调用`Dispose()`**: ```csharp var resource = new MyResource(); try { // 使用resource对象 } finally { resource.Dispose(); // 确保释放 } ``` #### 4. **关键注意事项** - **避免资源泄漏**:忘记调用`Dispose()`或未实现`IDisposable`会导致资源泄漏。 - **不要抛出异常**:`Dispose()`方法应避免抛出异常,否则可能掩盖其他错误[^3]。 - **终结器性能**:依赖终结器释放资源会影响性能,应优先显式释放。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值