处理释放IDisposable对象的通用函数

11

我正在开发一个涉及大量Sql对象的类 - 连接(Connection), 命令(Command), 数据适配器(DataAdapter), 命令生成器(CommandBuilder)等。我们有多个实例需要处理这样的代码:

if( command != null )
{
    command.Dispose();
}

if( dataAdapter != null )
{
    dataAdapter.Dispose();
}

etc

我知道这个问题的复制并不充分,但它已经开始有异味了。我认为出现异味的原因是,在某些情况下该对象也被设置为空(null)。

if( command != null )
{
    command.Dispose();
    command = null;
}

如果可能的话,我希望能够消除重复代码。我已经想出了这个通用的方法来处理对象并将其设置为null。

private void DisposeObject<TDisposable>( ref TDisposable disposableObject )
    where TDisposable : class, IDisposable
{
    if( disposableObject != null )
    {
        disposableObject.Dispose();
        disposableObject = null;
    }
}

我的问题是...

  1. 这个通用函数是否不好的想法?
  2. 把对象设置为 null 是否必要?

编辑:

我知道 using 语句,但由于某些成员变量需要比一个调用更长时间地存在,因此我不能总是使用它。例如连接和事务对象。

谢谢!


泛型并不是必需的;没有它们,代码会更简单。 - TamaMcGlinn
10个回答

7
你应该考虑是否可以使用 using 语句。
using (SqlCommand command = ...)
{
    // ...
}

这确保了在使用范围结束时调用命令对象上的Dispose。与自行编写清理代码相比,这具有许多优点:
  • 更加简洁。
  • 变量永远不会设置为null - 它在一个语句中声明和初始化。
  • 对象被处理后,变量就会超出范围,因此您减少了意外尝试访问已释放资源的风险。
  • 它是异常安全的。
  • 如果嵌套使用语句,则资源会按正确顺序(创建的相反顺序)自然地处置。

需要将对象设置为null吗?

通常不需要在使用完变量后将其设置为null。重要的是,在使用资源完成后调用Dispose。如果使用上述模式,则不仅不需要将变量设置为null - 这将导致编译错误:

Cannot assign to 'c' because it is a 'using variable'

需要注意的一件事是,只有在同一方法调用中获取和处理对象时,using才有效。如果您的资源需要保持活动状态超过一个方法调用的持续时间,则无法使用此模式。在这种情况下,您可能希望使您的类实现IDisposable,并确保在调用Dispose方法时清除资源。在这种情况下,您将需要像所写的代码一样。在此情况下,将变量设置为null不是错误的,但也不重要,因为垃圾回收器会正确地清理内存。重要的是确保在调用dispose方法时处置所有拥有的资源,并且您正在执行此操作。

一些实现细节:

  • 您应该确保如果两次调用Dispose,则不会引发异常。您的实用程序函数正确处理此情况。
  • 您应确保对象上的相关方法在对象已被处理后引发ObjectDisposedException

1
当然,这假定资源的生命周期仅为一个方法调用 - 这在此情况下不太可能,因为需要先检查变量是否为 null。 - James Curran
@James,你说得完全正确。@Mark,我已经更新了我的问题。 - Jerod Houghtelling

6
你应该在拥有这些字段的类中实现IDisposable。关于此主题,请参见我的博客文章。如果这不起作用,那么该类就不遵循OOP原则,需要进行重构。
在释放后将变量设置为null不必要的

我无法在大多数情况下使用using语句。我已经更新了我的问题。 - Jerod Houghtelling

3
如果你的对象需要进行大量清理工作,它们可能希望在一个单独的可处理列表中跟踪需要删除的内容,并一次性处理所有内容。这样,在拆卸时它就不需要记住所有需要处理的事情(也不需要检查是否为空,它只需查看列表即可)。
为了说明问题,你可以将 RecycleBin 包含在类中,但这可能无法构建。然后该类只需要处理垃圾箱即可。
public class RecycleBin : IDisposable
{
    private List<IDisposable> _toDispose = new List<IDisposable>();

    public void RememberToDispose(IDisposable disposable)
    {
        _toDispose.Add(disposable);
    }

    public void Dispose()
    {
        foreach(var d in _toDispose)
            d.Dispose();

        _toDispose.Clear();
    }
}

+1 这对我的当前情况不起作用,因为我不是一次性处理所有内容。虽然我喜欢这个建议,也许将来会用到它。 - Jerod Houghtelling

1

我假设这些是字段而不是局部变量,因此using关键字没有意义。

这个泛型函数是一个坏主意吗?

我认为这是一个好主意,我已经使用过类似的函数几次了。为使它通用化,加上+1。

有必要将对象设置为null吗?

从技术上讲,对象应允许多次调用其Dispose方法。(例如,在终结期间,如果对象被重新启用,则会发生这种情况。)在实践中,你可以选择相信这些类的作者还是编写防御性代码。个人而言,我先检查是否为空,然后再将引用设置为null。

编辑:如果此代码位于您自己的对象的Dispose方法中,则未能将引用设置为null不会泄漏内存。相反,它可作为防止重复释放的防御措施。


1

我假设您正在一个方法中创建资源,在另一个方法中释放它,并在一个或多个其他方法中使用它,这使得using语句对您无用。

在这种情况下,您的方法是完全正确的。

至于您问题的第二部分(“将其设置为null是否必要?”),简单的答案是“不必要,但也不会有任何影响”。

大多数对象持有一种资源--内存,由垃圾回收器处理释放,因此我们不必担心它。有些对象还持有其他资源:文件句柄、数据库连接等。对于第二类资源,我们必须实现IDisposable接口来释放该资源。

一旦调用Dispose方法,两个类别都是相同的:它们都持有内存。在这种情况下,我们可以让变量超出范围,丢弃对内存的引用,并允许GC最终释放它--或者我们可以通过将变量设置为null并明确丢弃对内存的引用来强制执行此问题。我们仍然必须等待GC启动才能实际释放内存,很可能变量在将其设置为null后不久就会超出范围,因此在绝大多数情况下,它根本没有任何影响,但在少数罕见情况下,它将使内存提前几秒钟被释放。
但是,在您特定的情况下,如果您正在检查null以查看是否应该调用Dispose,则如果有可能调用Dispose()两次,您可能应该将其设置为null。

1

鉴于iDisposable没有任何标准方法来确定对象是否已被处理,我喜欢在处理它们时将它们设置为null。确保处理已经被处理的对象是无害的,但是能够在监视窗口中检查对象并一眼看出哪些字段已被处理是很好的。此外,如果代码遵循在处理变量时将其置空(且仅在此时),则可以编写代码测试以确保应该已被处理的对象已被处理。


0

其他人已经推荐了使用using结构,我也推荐。但是,我想指出的是,即使您确实需要一个实用方法,也完全没有必要像您所做的那样将其泛型化。只需声明您的方法以接受IDisposable即可:

private static void DisposeObject( ref IDisposable disposableObject )
{
    if( disposableObject != null )
    {
        disposableObject.Dispose();
        disposableObject = null;
    }
}

我尝试过这个,但由于某些原因,我不得不将所有调用该方法的内容都进行强制转换。我同意这应该是有效的!我需要再次确认一下。 - Jerod Houghtelling
@Jerod:打包不起作用,因为您更改的是“disposable”而不是“mTransaction”的值。@JS:由于我们将要修改 disposableObject,编译器需要知道它的确切类型。 - James Curran
2
@Jerod - 在这种情况下,由于ref关键字的存在,方法参数不允许协变,否则例如您可以传入一个SqlConnection,然后在方法内将对象设置为FileStream的实例(这将违反参数的类型安全性)。 - John Rasch
1
@James,@John。非常好的观点!我知道它不被允许可能有一个原因。知道我为什么“被迫”使用通用接口而不是常规接口是很好的。 - Jerod Houghtelling
@John:比我的解释好多了(我一直在想“但将其设置为null应该没问题”——我没有考虑将其设置为完全不同的类型) - James Curran

0

您永远不需要将变量设置为nullIDisposable.Dispose的整个目的是将对象置于一种状态,使其可以在内存中无害地挂起,直到GC完成它,因此您只需“处理并忘记”。

我很好奇为什么您认为不能使用using语句。在一个方法中创建对象,在另一个方法中处理它是我的代码中非常糟糕的味道,因为您很快就会失去对哪里打开了什么的跟踪。最好像这样重构代码:

using(var xxx = whatever()) {
    LotsOfProcessing(xxx);
    EvenMoreProcessing(xxx);
    NowUseItAgain(xxx);
}

我相信这个模式有一个标准的名称,但我只是称它为“摧毁你所创造的一切,但不影响其他东西”。


我同意这段代码并不完美。这段代码来自一个SQL库类,被用于多个产品中。using语句不能正常工作的最大原因是因为需要事务支持的一个特性。库的用户告诉我们开始事务,然后他们可以进行多个数据操作,最后告诉我们提交事务。我认为这是一个非常合理的用例,因为它在客户端代码和SQL特定代码之间创建了一个分离。 - Jerod Houghtelling
@Jerod,难道不可能在一个方法中创建连接、开始事务、调用所有库方法、提交事务并释放连接吗?你所描述的正是我设计SQL库类的方式,但我仍然可以使用“using”语句。 - Christian Hayter
可以实现这一点,除非用户需要了解我们库的复杂细节。不幸的是,我目前没有自由更改我们的公共API,只能使用手头的资源。 - Jerod Houghtelling
@Jerod,用户的调用代码应该包含using语句,而不是你的API。我并不建议更改你的API。也许我们最好让这个对话结束,因为除非我能看到你的API,否则我将无法提供更多有用的信息。 :-) - Christian Hayter

0

0

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