【问题标题】:How to do C++ style destructors in C#?如何在 C# 中执行 C++ 风格的析构函数?
【发布时间】:2008-09-06 16:12:48
【问题描述】:

我通过IDisposable 获得了一个带有Dispose 函数的C# 类。它旨在用于using 块内,以便可以立即释放它处理的昂贵资源。

问题是在调用Dispose之前抛出异常,程序员忽略了使用usingfinally时出现了错误。

在 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++。有 20 个嵌套 using 语句吗?

【问题讨论】:

    标签: c# dispose idisposable using


    【解决方案1】:

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

    • 每个 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;
    }
    

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

    【讨论】:

    • 向每个实现 IDisposable 的对象添加终结器,以便您可以在调用终结器时进行记录,这不是一个好主意。终结器会为您的对象增加成本(它会在额外的 GC 循环中存活),并且您可以对内部状态做出的大多数假设都是无效的。
    • “在我工作的地方,我们使用以下准则:每个 IDisposable 类都必须有一个终结器...”更改您的准则。
    • 我不喜欢带有终结器的类持有对其他对象的引用,即使终结器不应该运行。我认为最好定义一个 FinalizationSquawker 类,如果它被最终确定,它将发出尖叫声,并让需要处置的类在初始化时创建一个 FinalizationSquawker 并在它们被处置时处置它。顺便说一句,我建议使用 Interlocked.Exchange 作为已处置的标志,而不是使用锁。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      @吵架

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

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

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

      【讨论】:

        【解决方案4】:
        ~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 将其肮脏的手套放在上面时(包括应用程序关闭或意外结束)。

        【讨论】:

          【解决方案5】:

          最佳做法是在课堂上使用终结器并始终使用 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# 非常擅长管理自己的内存,即使在处理不当的情况下也是如此。如果您经常处理非托管资源,那么它就不那么强大了。

          【讨论】:

            【解决方案6】:

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

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

            【讨论】:

            • "IDisposable 仅适用于非托管资源..." 如果我​​在其构造函数中编写一个订阅事件的类怎么办?我需要一个相应的地方可以退订,而客户需要一种标准的方式来要求我清理这些东西。否则我的对象可能会无限期地保持活力。
            • 在这种情况下,您的论点是事件属于“非托管”类别。
            • Joel->错误 - 如果您有一个需要或使用内存的非托管对象,那么您必须小心处理模式和内存压力的使用。忽略它会导致:非托管资源占用 gc 想要的内存或非托管分配失败,因为 GC 占用内存。
            • @supercat- 根据这种推理,似乎每个修改状态的方法都必须设为私有,并由一个单独的公共方法包装,该方法捕获它抛出的任何异常并展开任何部分状态更改。那么为什么不把这个处理放在方法本身中,这样最有意义呢?
            • @Daniel Earwicker:在 vb.net 中,可以设计一个类,即使在字段初始值设定项中也可以安全地分配 IDisposables,但前提是构造函数被包装。在 vb.net 中,如果基类构造函数创建 IDisposable 并且派生类初始化程序抛出,则确定性地处理基类的 IDisposables 的唯一方法是包装构造函数。在 C# 中,将每个构造函数的代码包含在一个处理部分构造对象的 try-catch 中可能是尽善尽美的。这必须在派生类中完成以避免泄漏基对象。
            【解决方案7】:

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

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2016-04-20
              • 2017-06-12
              • 1970-01-01
              • 1970-01-01
              • 2017-12-01
              • 2014-05-12
              • 2020-04-10
              相关资源
              最近更新 更多