IDisposable是否应该级联应用?

15

这是一个相当基础的问题,但我还是有点困惑。

IDisposable 被实现时,是为了让对象的用户在对象最终被垃圾回收之前释放底层资源(例如套接字等)。

当我拥有一个持有 DbConnection(实现了 IDisposable)的类时,我的类是否需要实现 IDisposable 并将调用链接到 DbConnection 或任何其他它拥有的 IDisposable 对象?否则,DbConnection 的资源只能在我的类被垃圾回收时才会被释放,因此当我的类丢弃对连接的引用并 GC 完成对 DbConnection 的终结时,DbConnection 的资源才会被释放。

9个回答

10

是的,如果您控制可以释放的对象,则始终要实现IDisposable。 总是。 如果不这样做,您的代码不会出错,但是不这样做会使拥有可释放对象的目的失去意义。

GC优化的一般规则是:

  • 任何控制非GC管理对象的类都必须实现finalizer(一般也应该实现IDisposable)。 这就是“顶级”可释放类通常来自于的地方 - 他们通常控制着一个窗口,套接字,互斥量或其他东西的句柄。
  • 任何实例化IDisposable成员的类都应该实现IDisposable本身,并正确Dispose()其组件。
  • 任何实例化IDisposable对象的函数在使用完后应该正确Dispose()。 不要让它只是超出范围而被清除。

如果您正在为自己编写应用程序,则可以弯曲或忽略这些规则,但是当向其他人分发代码时,您应该专业并遵循规则。

这里的逻辑是,当您在GC视图之外控制内存时,GC引擎无法正确管理您的内存使用情况。例如,在您的.NET堆上,您可能只有4个字节的指针,但在非托管的领域中,您可能有200 MB的内存被指向。 GC引擎不会尝试收集这些内容,直到您有几十个,因为它所看到的只是一些字节; 而在现实世界中,它看起来很像内存泄漏。

因此,规则是,在您完成使用它时应立即释放非托管内存(IDisposable链为您执行此操作),而GC引擎会在其自己的时间内释放托管内存。


1
那么有趣的是,DataSet.Dispose()没有处理Dispose()它的DataTables,这不奇怪吗? - Nariman
2
这个问题应该被语言更好地处理-不得不在类层次结构中连锁使用IDisposable/Dispose是荒谬和乏味的。 - nicodemus13

6

如果一个类需要处理使用的任何对象,则需要实现IDisposable接口。一个例子就是StreamReader,它实现了IDisposable接口以便可以处理其相关联的流对象。


问题是它是否需要处理任何对象。它可能需要。您如何看待DbConnection示例? - Johannes Rudolph
@Johannes:一个例子就是你的类。它应该实现IDisposable接口,这样它就可以处理其关联的DbConnection对象的释放。 - Powerlord
(是的,我有意改述了David所说的话) - Powerlord

3
如果我理解您的问题正确,您有一个使用DbConnection的类。您希望确保在完成工作或销毁类时正确处理DbConnection。有几种方法可以实现这一点。
如果您在方法中将数据库连接用作局部变量,则可以利用using() {}语句。 using (SqlConnection sqlConnection = new SqlConnection(connStr))
{
...在此处使用连接
}
using () {}语句会自动调用()中声明的对象的Dispose()方法。(它还要求在()中声明的对象实现IDisposable以确保它们可以被处理)
如果您是将DbConnection作为私有变量进行初始化,则可能需要自己实现IDisposable,然后在Dispose()方法中调用_dbConnection.Dispose()。这样,当您的对象被处理时,db连接对象也将被处理。 public class MyDALObj : IDisposable
{
public MyDalObj()
{
... 创建_dbConn对象 ...
}
public void Dispose()
{
_dbConn.Dispose();
}
private DbConnection _dbConn;
}

3

你应该这样做,因为这是确保你的类使用者正确处理内部资源的唯一方法。

然而,在这种情况下,Dispose()所使用的模式可能与通常写的略有不同,因为你不必区分非托管资源和托管资源(你封装的资源始终被视为“托管”资源)。

我在这个特定主题上撰写了一篇详细的博客文章 - 封装IDisposable资源


2
有两种不同的情况:
1. 对象通过构造函数参数或属性获得一个对象引用,并且该对象实现了IDisposable接口。
2. 对象构造了一个实现了IDisposable接口的对象实例。
在第二种情况下,您的对象负责涉及的资源,所以您的对象必须实现IDisposable接口,在处理完后应该释放您构造的对象。
您的DbConnection属于第二种情况,因此是的,您的对象应该实现IDisposable接口,并释放连接。
在第一种情况下,您需要决定以下三个解决方案:
1. 您的对象只引用外部对象。您的对象不应该释放这个外部对象。对于这种情况,您不需要为此特定对象实现IDisposable接口(如果您还在内部构造可释放对象,则回到上述第二种情况)。
2. 您的对象负责外部对象。在这种情况下,即使您的对象没有构造此外部对象,您也会回到第二种情况。在这里,您实现IDisposable接口,并释放给您的对象。
3. 您需要实现一种方式,让外界告诉您选择哪种解决方案。例如,构造函数可能会提供连接和布尔参数(或理想情况下是枚举值),告诉构造函数是否拥有提供的连接。在这里,您还需要实现IDisposable接口,但在Dispose方法中,您需要检查所有权,并仅在拥有连接时才释放提供的连接。
总之:
1. 您拥有的对象需要释放。
2. 您不拥有的对象不需要释放。
还有第三种情况,听起来似乎您没有遇到过,但是无论如何。
在单个方法内局部构造、使用和丢弃对象,而不传递或将其存储在类字段中时,您需要使用using语句,如下所示:
using (IDbConnection conn = ....())
{
}

0

这绝对是最佳实践,特别是在处理大型/非托管对象时。

编辑:最佳实践,但不是强制性的。


0

由于我们无法确定对象何时被GC收集,因此我们使用IDisposable接口来有机会在对象被垃圾回收之前有意释放非托管资源。 如果一个可处理的对象在被收集之前没有被处理,那么它的资源可能直到AppDomain退出才被释放。 几乎是一条不成文的规则,每个引用IDisposable对象的对象本身都应该是IDisposable,并在其自己的Dispose方法中调用其IDisposable引用的Dispose方法。


0
当然,如果使用C++/CLI,您可以消除IDisposable的很多(重新)实现成本,并获得接近于管理堆上对象的确定性终结。 这是一种语言常常被忽视的方面,许多人似乎将其归为“仅用于粘合代码”的类别。

我不熟悉C++/CLI,但它听起来像是一种有趣的语言。它是否处理了一些vb.net和C#无法提供合理解决方案的问题情况(例如,在派生类构造函数中抛出异常时清理部分构造对象)?如果是这样,这种处理是否仅限于基类和派生类都用C++/CLI编写的情况,还是在其中一个或另一个(或基类和子派生类都)使用其他语言编写的情况下也可以提供此类保护? - supercat

0

当您使用Dispose提供显式控制时,应使用Finalize方法提供隐式清理。如果程序员未调用Dispose,则Finalize提供了一种备份,以防止资源永久泄漏。

我认为最好的实现方式是使用Dispose和Finalize方法的组合。您可以在这里找到更多信息。


1
Finalizers提供了最后一次机会,如果Dispose()方法没有被调用,可以释放您自己的未托管资源。只有在需要清理未托管资源时才实现finalizer。Finalizer不会调用成员对象的Dispose()方法。相反,您的dispose方法应该调用您的finalizer(然后抑制终结)。 - tylerl
精确地说,因为在终结器中对其他对象的引用不能保证是有效的。它们可能已经被收集了。 - Johannes Rudolph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接