【问题标题】:how to dispose C# base class when derived class constructor generates an error当派生类构造函数产生错误时如何处理C#基类
【发布时间】:2013-01-17 01:14:52
【问题描述】:

如果 C# 派生的 IDisposable 类构造函数产生错误,如何处置已经完全构造的 IDisposable 基类?

由于类层次结构中的所有字段在任何构造函数执行之前都已初始化,派生构造函数调用 base.Dispose() 是否安全?它违反了在对象完全构造之前不调用虚方法的规则,但我想不出另一种方法来做到这一点,而且我的搜索没有发现任何关于这种情况的信息。

【问题讨论】:

  • 如果可以帮助构造函数不应该抛出异常。改为使用 init 方法。
  • 构造函数不应调用不需要能够处理部分构造的对象的虚拟方法。为可继承类正确编写的 dispose 方法通常必须能够处理部分构造的对象,因为这是防止泄漏的最实用方法。
  • @supercat:在所有构造函数运行之前,我没有意识到虚函数表已经到位。假设 Dispose 方法被编写为处理部分构造的对象(我的是),您希望将派生类标记为已密封,以便某人不会从该类继承并在他们的类中定义一个未处理部分的虚拟 dispose 方法构造对象。
  • @jimvfr:除非派生类有理由对子类比它的基类更偏执,否则为什么要假设即使它可以遵循基类契约,子派生类也不会能够?我认为应该主要根据其他基础做出是否密封类的决定。

标签: c# constructor dispose


【解决方案1】:

我的观点是构造函数应该是轻量级的,而不是依赖可能引发异常的外部资源/等。构造函数应该做足够的工作来验证 Dispose() 可以安全地调用。考虑使用包含而不是继承,或者让工厂方法完成可能引发的工作。

【讨论】:

  • 包含不提供可替代性,工厂方法对继承也没有真正的帮助。即使一个类没有任何公共构造函数并且需要通过工厂方法生成所有该类型的实例,生成任何派生类型实例的唯一方法是通过链式构造函数,并且没有很好的方法让派生类构造函数只能通过基础中的工厂代码调用。有一些方法,但它们很恶心。
【解决方案2】:

如果所有派生类构造函数通过异常退出,则必须对正在构造的对象调用Dispose。此外,如果字段初始值设定项构造 IDisposable 实例或可能失败,则很难编写防泄漏类。太糟糕了,因为要求对象在一个地方声明,在第二个地方初始化,在第三个地方清理,这并不是连贯代码的秘诀。

我建议最好的模式可能是这样的:

class foo : baseFoo , IDisposable
{
    foo () : baseFoo
    {
        bool ok = false;
        try
        {
            do_stuff();
            ok = true; // Only after last thing that can cause failure
        }
        finally
        {
            if (!ok)
              Dispose();
        }
    }
}

请注意,C++/CLI 会自动实现该模式,并且还可以自动处理 IDisposable 字段的清理。太糟糕了,这种语言在其他方面看起来很痛苦。

PS--除了相对较少的例外,主要围绕可预测成本的共享不可变对象(例如画笔、字体、小位图等),依赖Finalize 清理对象的代码已损坏。如果创建了 IDisposable,则必须将其释放,除非创建它的代码特别了解将释放延迟到终结器的后果。

【讨论】:

  • 从构造函数中不调用虚方法(Dispose() 调用虚方法 Dispose(bool) )不是规定吗?
  • @jimvfr:不应调用虚拟方法除非这些方法已准备好在部分构建的对象上被调用的可能性。一般来说,作为基类契约的一部分,要求所有子类的 Dispose 代码必须可用于部分构建的对象(例如,使用在其目标上调用 Dispose 的静态 SafeDispose 方法)通常更容易且更可靠如果非空)并在所有构造失败时调用Dispose,而不是尝试手动编写代码,该代码仅检查和处理到目前为止已设置的IDisposable字段。
  • @jimvfr:这在某种程度上预先假定所有需要清理的东西都以这样的方式实现IDisposable,这样多次调用Dispose 是无害的,但微软的文档表明@ 987654332@ 对象应该以这种方式运行。它并没有说当Dispose 被调用两次时做坏事的对象是“损坏的”,但我认为在 GC 系统中没有理由不让所有对象在面对多个 @ 时都安全987654334@ 来电。
  • 必须对对象构造过程进行这种手动控制并不理想,但我认为没有比这个答案更好的处理这种情况的方法了。假设任何可能引发初始化的字段都将在构造函数的“try”块中分配,那么处理由字段初始化程序初始化的字段有什么问题?我不会检查 bool 标志,而是在“try/catch all”块中执行构造函数代码,其中 catch 只是调用 Dispose 并重新抛出,因为这样代码更少,效率略高。
  • @Neutrino:一个困难是基类或派生类构造函数中可能发生异常。如果派生类使用字段初始化器语法初始化任何IDisposable 字段,则在基构造函数抛出时提供及时清理的唯一方法是让基构造函数调用Dispose。我认为语言不允许类将整个构造过程包装在 try/catch 块中并在抛出时调用用户指定的方法没有任何理由,但 C# 和 VB.NET 都没有这样做。顺便说一句,我希望个别构造函数有一个访问说明符......
【解决方案3】:

对于托管资源,这应该不是问题,它们会被垃圾收集。对于非托管资源,请确保为对象定义了终结器,这将确保清理非托管资源。

另外,从构造函数中抛出异常被认为是非常不礼貌的,最好提供一个工厂方法来进行构造和错误处理,或者为您的对象配备一个将抛出实际异常的初始化方法。这样构建总是成功的,你不会遇到这些类型的问题。


正确,垃圾收集器不调用 Dispose,但 Finalizer 是,它又需要调用 Dispose。这是一种更昂贵的技术,除非使用得当。我的回答中没有这样说,是吗;)。

您可以调用 GC 来强制运行收集,并且可以等待所有待处理的终结器。最好不要将生成异常的代码放在构造函数中,或者在构造函数中的代码周围放置一个 try/catch,以确保在发生错误时对这些文件调用 Dispose。以后可以随时重新抛出异常。

【讨论】:

  • (参见我对 Frank Schwieterman 的回复)我将使用工厂方法
  • 在我的例子中,基类使用了一些作为托管资源的 FileStream,但需要在出错时立即处理,我等不及文件稍后关闭。 GC 也不会调用 Dispose。
  • 终结器应该只清理非托管资源(即让终结器方法调用 Dispose(false)),所以我看不出终结器如何清理托管资源。
  • 在这种情况下(我认为这是设计中的错误),您可以调用 Dispose(true) 来解锁文件。虽然最好确保构造函数中永远不会发生异常(或者文件没有​​在构造函数中打开)。
  • 同意最好确保构造函数不能抛出,这就是我在第一条评论中提到的切换到工厂方法的原因。然后工厂方法将调用 Initialize()。
【解决方案4】:

此解决方案基于@supercat 的提议。任何可能抛出的成员的初始化必须在构造函数的 try/catch 块中执行。如果满足该条件,则任何构造函数抛出的异常都会正确处理完全或部分构造的基类或派生类。

在此测试代码中,依次取消注释四个异常,程序将输出由于构造函数抛出异常而未正确释放的 Disposable 资源。然后取消注释两个 Dispose 调用并观察所有内容都已按应有的方式清理。

    class DisposableResource : IDisposable
    {
        public DisposableResource(string id) { Id = id; }
        ~DisposableResource() { Console.WriteLine(Id + " wasn't disposed.\n"); }
        public string Id { get; private set; }
        public void Dispose() { GC.SuppressFinalize(this); }
    }

    class Base : IDisposable
    {
        public Base()
        {
            try
            {
                throw new Exception();      // Exception 1.
                _baseCtorInit = new DisposableResource("_baseCtorInit");
//              throw new Exception();      // Exception 2.
            }
            catch(Exception)
            {
//              Dispose();                  // Uncomment to perform cleanup.
                throw;
            }
        }

        public virtual void Dispose()
        {
            if (_baseFieldInit != null)
            {
                _baseFieldInit.Dispose();
                _baseFieldInit = null;
            }

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

        private DisposableResource _baseFieldInit = new DisposableResource("_baseFieldInit");
        private DisposableResource _baseCtorInit;
    }

    class Derived : Base
    {
        public Derived()
        {
            try
            {
//              throw new Exception();      // Exception 3.
                _derivedCtorInit = new DisposableResource("_derivedCtorInit");
//              throw new Exception();
            }
            catch (Exception)
            {
//              Dispose();                  // Uncomment to perform cleanup.
                throw;
            }
        }

        public override void Dispose()
        {
            if (_derivedFieldInit != null)
            {
                _derivedFieldInit.Dispose();
                _derivedFieldInit = null;
            }

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

            base.Dispose();
        }

        private DisposableResource _derivedFieldInit = new DisposableResource("_derivedFieldInit");
        private DisposableResource _derivedCtorInit;
    }

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Derived d = new Derived();
            }
            catch (Exception)
            {
                Console.WriteLine("Caught Exception.\n");
            }

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            Console.WriteLine("\n\nPress any key to continue...\n");
            Console.ReadKey(false);
        }
    }

【讨论】:

  • 如果你定义一个方法void DisposeAndClear<T>(ref T it) where T:class {T oldValue = Interlocked.Exchange(ref it, null); if (oldValue) oldValue.Dispose(); },我建议你可以消除一些冗余代码。然后你可以简单的DisposeAndClear每个字段,不管它是否已经为空;此外,即使Dispose 以某种方式在多个线程上被调用,每个字段仍然只会被释放一次。
  • 这是个好主意,但我没有在我的示例中添加多线程支持,以使关键原理的演示尽可能简单。
  • 我的重点不是Interlocked.Exchange,而是每个字段的垂直空间将从6行(例如if (_baseFieldInit)...)减少到1行(例如DisposeAndClear(ref _baseFieldInit);) .即使只有两个字段,我认为DisposeAndClear 方法会提高清晰度,因为每个字段都会被提及一次而不是三次。每增加一个字段,就可以再节省五行垂直空间。如果一个类有六个字段,那么在恕我直言,一个主体适合六行的处理方法比一个需要 35 行的处理方法更具可读性。
猜你喜欢
  • 2023-04-10
  • 1970-01-01
  • 1970-01-01
  • 2015-08-18
  • 2011-02-02
  • 1970-01-01
  • 1970-01-01
  • 2016-07-19
  • 1970-01-01
相关资源
最近更新 更多