2

我有一个带有Dispose函数的 C# 类IDisposable。它旨在在using块内使用,因此可以立即释放它处理的昂贵资源。

问题是在调用之前抛出异常时发生了错误Dispose,而程序员忽略了使用usingor finally

在 C++ 中,我从来不用担心这个。对类的析构函数的调用将自动插入到对象作用域的末尾。避免这种情况发生的唯一方法是使用 new 运算符并将对象保存在指针后面,但这需要程序员额外的工作不是他们会偶然做的事情,比如忘记使用using.

有什么方法using可以在 C# 中自动使用块?

非常感谢。

更新:

我想解释一下为什么我不接受终结者的答案。这些答案本身在技术上是正确的,但它们不是 C++ 风格的析构函数。

这是我发现的错误,简化为基本要素...

try
{
    PleaseDisposeMe a = new PleaseDisposeMe();
    throw new Exception();
    a.Dispose();
}
catch (Exception ex)
{
    Log(ex);
}

// This next call will throw a time-out exception unless the GC
// runs a.Dispose in time.
PleaseDisposeMe b = new PleaseDisposeMe();

使用FXCop是一个很好的建议,但如果这是我唯一的答案,我的问题将不得不成为 C# 人的请求,或者使用 C++。二十个嵌套 using 语句有人吗?

4

7 回答 7

6

在我工作的地方,我们使用以下准则:

  • 每个 IDisposable 类都必须有一个终结器
  • 无论何时使用 IDisposable 对象,都必须在“使用”块中使用它。唯一的例外是对象是另一个类的成员,在这种情况下,包含类必须是 IDisposable 并且必须在其自己的“Dispose”实现中调用该成员的“Dispose”方法。这意味着开发人员永远不应调用“Dispose”,除非在另一个“Dispose”方法中调用,从而消除了问题中描述的错误。
  • 每个终结器中的代码必须以警告/错误日志开头,通知我们终结器已被调用。这样,在发布代码之前,您就有很大的机会发现上述错误,而且这可能是您系统中发生错误的提示。

为了让我们的生活更轻松,我们的基础架构中还有一个 SafeDispose 方法,它在 try-catch 块中调用其参数的 Dispose 方法(带有错误日志记录),以防万一(尽管 Dispose 方法不应该抛出异常)。

另请参阅:Chris Lyon关于 IDisposable 的建议

编辑:@Quarrelsome:你应该做的一件事是在'Dispose'中调用 GC.SuppressFinalize,这样如果对象被释放,它就不会被“重新释放”。

通常还建议持有一个标志,指示对象是否已被处置。以下模式通常非常好:

class MyDisposable: IDisposable {
    public void Dispose() {
        lock(this) {
            if (disposed) {
                return;
            }

            disposed = true;
        }

        GC.SuppressFinalize(this);

        // Do actual disposing here ...
    }

    private bool disposed = false;
}

当然,锁定并不总是必要的,但如果您不确定您的类是否会在多线程环境中使用,建议保留它。

于 2008-09-06T17:03:13.733 回答
3

不幸的是,没有任何方法可以直接在代码中执行此操作。如果这是内部问题,则有各种代码分析解决方案可以解决此类问题。你看过 FxCop 吗?我认为这将捕获这些情况以及 IDisposable 对象可能被挂起的所有情况。如果它是人们在您的组织之外使用的组件,并且您不需要 FxCop,那么文档确实是您唯一的资源 :)。

编辑:在终结器的情况下,这并不能真正保证何时会发生终结。因此,这可能是您的解决方案,但这取决于具体情况。

于 2008-09-06T16:17:36.433 回答
2
~ClassName()
{
}

编辑(粗体):

当对象移出范围并由垃圾收集器整理时,将调用 if ,但这不是确定性的,也不能保证在任何特定时间发生。这称为终结器。所有带有终结器的对象都会被垃圾收集器放入一个特殊的终结队列中,并在其中调用它们的终结方法(因此从技术上讲,声明空终结器会影响性能)。

根据框架指南,“接受”的处置模式如下与非托管资源:

    public class DisposableFinalisableClass : IDisposable
    {
        ~DisposableFinalisableClass()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // tidy managed resources
            }

            // tidy unmanaged resources
        }
    }

所以上面的意思是,如果有人调用 Dispose ,非托管资源就会被整理。但是,如果有人忘记调用 Dispose 或阻止调用 Dispose 的异常,非托管资源仍将被清理掉,只是稍晚一点,当 GC 获得其肮脏的手套时(包括应用程序关闭或意外结束)。

于 2008-09-06T16:16:40.180 回答
2

@争吵

当对象移出范围并由垃圾收集器整理时,将调用 if。

该声明具有误导性,并且我的阅读方式不正确:绝对不能保证何时调用终结器。billpg 应该实现终结器是绝对正确的;但是,当对象超出他想要的范围时,它不会被自动调用。 证据表明,Finalize 操作下的第一个要点有以下限制

事实上,微软授予 Chris Sells 创建一个使用引用计数而不是垃圾收集Link的 .NET 实现。事实证明,这对性能造成了相当大的影响。

于 2008-09-06T16:34:56.593 回答
2

最佳实践是在课堂上使用终结器并始终使用using块。

虽然没有真正的直接等价物,终结器看起来像 C 析构函数,但行为不同。

你应该嵌套using块,这就是为什么 C# 代码布局默认将它们放在同一行...

using (SqlConnection con = new SqlConnection("DB con str") )
using (SqlCommand com = new SqlCommand( con, "sql query") )
{
    //now code is indented one level
    //technically we're nested twice
}

当您不使用时using,您无论如何都可以做它在引擎盖下所做的事情:

PleaseDisposeMe a;
try
{
    a = new PleaseDisposeMe();
    throw new Exception();
}
catch (Exception ex) { Log(ex); }  
finally {    
    //this always executes, even with the exception
    a.Dispose(); 
}

对于托管代码,C# 非常擅长管理自己的内存,即使在处理不当的情况下也是如此。如果您经常处理非托管资源,那么它就不那么强大了。

于 2008-09-07T16:25:26.077 回答
0

这与程序员忘记在 C++ 中使用delete没有什么不同,只是至少在这里垃圾收集器最终仍会赶上它。

如果您担心的唯一资源是内存,那么您永远不需要使用 IDisposable。该框架将自行处理。IDisposable 仅适用于非托管资源,如数据库连接、文件流、套接字等。

于 2008-09-06T16:27:39.840 回答
0

更好的设计是让这个类在处理之前自行释放昂贵的资源。

例如,如果它是一个数据库连接,则仅在需要时连接并立即释放,远在实际类被释放之前。

于 2008-09-06T16:48:04.347 回答