在终结器中,缺少外部程序集的成员的IDisisable失败

m0s*_*0sa 1 .net c# dll idisposable finalizer

我有2个程序集,A包含Main方法和Foo类,它使用Bar来自程序集B的类:

杆组件(组件B):

public sealed class Bar : IDisposable { 
    /* ... */ 
    public void Dispose() { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)

Foo类(程序集A):

public class Foo : IDisposable {
    private readonly Bar external;
    private bool disposed;
    public Foo()
    { 
        Console.WriteLine("Foo");
        external = new Bar(); 
    }
    ~Foo()
    { 
        Console.WriteLine("~Foo");
        this.Dispose(false); 
    }
    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (disposed) return;
        if (disposing) external.Dispose();
        disposed = true;
    }
}
Run Code Online (Sandbox Code Playgroud)

入口点(在程序集A中):

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var foo = new Foo();
            Console.WriteLine(foo);
        }
        catch (FileNotFoundException ex) 
        {
            // handle exception
            Console.WriteLine(ex.ToString());
        }
        Console.ReadLine();
    }
}
Run Code Online (Sandbox Code Playgroud)

这个软件的一个要求是它必须在缺少dll时优雅地处理这个案例.

因此,当我删除程序集B并启动应用程序时,我希望main方法中的try catch块处理当程序集B丢失时抛出的FileNotFoundException.它有点像,但这就是问题的起源......

当应用程序继续(在控制台中输入一行)时,会调用Foo类的终结器(?!),尽管没有创建Foo实例 - 尚未调用构造函数.由于没有该类的实例,因此无法在外部调用实例上的GC.SupressFinalize.在没有B程序集的情况下运行项目时,您在控制台输出中看到的唯一内容是~Foo.

所以问题:

  • 为什么即使没有创建类的实例,也会调用终结器?(对我来说,这完全没有意义!我很想得到开悟)
  • 是否可以在终结器中没有try-catch块的情况下防止应用程序崩溃?(这意味着重构整个代码库......)

一些背景:我在编写插件启用企业应用程序时遇到了这个问题,如果插件部署文件夹中缺少dll并标记有故障的插件,则必须继续操作.我认为围绕外部插件加载过程的try-catch块就足够了,但显然它没有,因为在捕获第一个异常之后仍然会调用终结器(在GC线程上),最终崩溃了应用程序.

备注上面的代码是我可以写的最简约的代码,用于在终结器中重现异常.

备注2如果我在Foo构造函数中设置断点(在删除Bar的dll之后),则不会命中它.这意味着如果我在构造函数中设置一个创建关键资源的语句(在新建Bar之前),它将不会被执行,因此不需要调用终结器:

// in class Foo
public Foo() {
    // ...
    other = new OtherResource(); // this is not called when Bar's dll is missing
    external = new Bar();        // runtime throws before entering the constructor
}

protected virtual void Dispose(bool disposing) {
    // ...
    other.Dispose();    // doesn't get called either, since I am
    external.Dispose(); // invoking a method on external
    // ...
}
Run Code Online (Sandbox Code Playgroud)

备注3 一个明显的解决方案是实现IDisposable,如下所示,但这意味着打破参考模式实现(即使FxCop会抱怨).

public abstract class DisposableBase : IDisposable {
    private readonly bool constructed;
    protected DisposableBase() {
        constructed = true;
    }
    ~DisposableBase() {
        if(!constructed) return;
        this.Dispose(false);
    } 
    /* ... */
}   
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 10

为什么即使没有创建类的实例,也会调用终结器?

这个问题毫无意义.显然是创建了一个实例; 如果没有创建实例,终结器将最终完成什么?您是否试图告诉我们该终结器中没有"此"参考?

尚未调用构造函数

无法调用构造函数,因为jitting构造函数引用了缺少类型的字段.怎样才能调用甚至不能被jitted的构造函数体?

您似乎认为仅仅因为无法调用构造函数,无法创建实例.这根本不符合逻辑.显然,调用ctor 之前必须有一个实例,因为对该实例的引用作为"this"传递给它.因此内存管理器创建一个实例 - 垃圾收集器知道已分配内存 - 然后它调用构造函数.如果调用构造函数抛出异常 - 或者由异步异常(例如线程中止)中断 - 那里仍然存在垃圾收集器已知的实例,因此在死亡时需要完成.

由于该对象永远不会被分配给任何实时变量 - 它不可能,因为赋值发生在ctor之后,并且当抖动试图使它jit时ctor抛出 - 它将被确定为在下一代零时死亡采集.然后将它放到终结器队列中,这将使其保持活动状态.

仍然会调用终结器(在GC线程上),最终崩溃应用程序.

然后修复终结器,使其不这样做.

请记住,ctor可以随时被异步异常中断,例如线程中止.您不能依赖于终结器中维护的对象的任何不变量.终结器是非常奇怪的代码; 您应该假设它们可以在任意线程上以任意顺序运行,并且对象处于任意不良状态.您需要在终结器中编写极其防御性的代码.

如果我在Foo构造函数中设置断点(在删除Bar的dll之后),则不会命中它.

正确.正如我所说,构造函数体甚至不能被jitted.你怎么能在一个甚至无法进行jitted的方法中找到一个断点?

这意味着如果我在构造函数中设置一个创建关键资源的语句(在新建Bar之前),它将不会被执行,因此不需要调用终结器.

无论想终结需要被称为是完全不相干的垃圾收集器.终结器可能具有其他语义,而不仅仅是清理资源.垃圾收集器不会尝试在心理上确定开发人员的意图,并决定是否需要调用终结器. 对象被分配并有一个终结器,你没有压制它的最终确定,所以它将被最终确定.如果您不喜欢,那么不要制作终结者.你做了一个终结器,因为大概你想要完成对象的所有实例,所以它们就是这样.

坦率地说,我会重温你的基本情景.您可以安全地恢复并继续在appdomain中执行代码的想法,这对我来说似乎是一个非常糟糕的主意.实现这一目标将非常困难.