c# - IDisposable with member from missing external assembly 在终结器中失败

标签 c# .net dll idisposable finalizer

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

条形组件(组件 B):

public sealed class Bar : IDisposable { 
    /* ... */ 
    public void Dispose() { /* ... */ }
}

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;
    }
}

入口点(在程序集 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();
    }
}

这个软件的要求之一是它必须优雅地处理 dll 丢失的情况。

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

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

所以问题:

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

一些背景: 我在编写启用插件的企业应用程序时遇到了这个问题,该应用程序要求如果插件部署文件夹中缺少 dll 并标记有故障的插件,它必须继续运行。我认为围绕外部插件加载过程的 try-catch block 就足够了,但显然它不够,因为在捕获第一个异常之后,终结器仍然被调用(在 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
    // ...
}

备注3 一个明显的解决方案是像下面那样实现 IDisposable,但这意味着破坏引用模式实现(即使 FxCop 也会提示)。

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

最佳答案

Why is the finalizer called even though no instance of the class is created?

这个问题毫无意义。显然创建了一个实例;如果没有创建实例,终结器将完成什么?你是想告诉我们终结器中没有“this”引用吗?

the constructor hasn't been called

无法调用构造函数,因为构建函数引用了一个缺少类型的字段。连jitt都不能调用的构造函数体怎么调用?

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

由于对象永远不会被分配给任何事件变量——它不可能,因为分配发生在 ctor 之后,并且当 jitter 试图 jit 它时 ctor 抛出——它将被确定为死的下一代零集合。然后它将被放入终结器队列,这将使它存活。

the finalizer is still invoked (on the GC thread), which finally crashes the application.

然后修复终结器,使其不会那样做。

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

If I set the breakpoint in the Foo constructor (after deleting Bar's dll) it is not hit.

正确。正如我所说,构造函数体甚至不能被 jitted。你怎么能在一个连 jitt 都无法执行的方法中设置断点?

This means if I would set have a statement in the constructor that creates a critical resource (before newing up Bar) it wouldn't be executed, hence no need for the finalizer to be called.

是否认为需要调用终结器与垃圾收集器完全无关。终结器可能具有其他语义,而不仅仅是清理资源。垃圾收集器不会尝试从心理上确定开发人员的意图并决定是否需要调用终结器。 该对象已分配并具有终结器,并且您没有抑制其终结器,因此它将被终结。如果您不喜欢这样,那么不要创建终结器。 em> 你制作了一个终结器,因为你可能希望对象的所有实例都被终结,所以它们将会被终结。

坦率地说,我会重新审视您的基本情况。您可以安全地恢复并继续在缺少所需 DLL 的应用程序域中执行代码的想法对我来说似乎是一个非常糟糕的主意。做到这一点将非常困难。

关于c# - IDisposable with member from missing external assembly 在终结器中失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7540433/

相关文章:

c# - 从组合枚举值中获取所有枚举常量的更好方法

c# - 无法加载文件或程序集 'Microsoft.Data.Edm'

c# - 我应该使用本地数据库还是 XML 文件?

.net - 每个文件只执行一个类

database - 我不确定访问数据库的固定方式是什么

c++ - 从 DLL 调用 MATLAB

c# - 使用 DropDownList 填充 Gridview

c# - 如何使用 C# 打印 pdf

.net - LINQ-to-SQL 中存储过程的问题

c++ - 当前路径的 loadlibrary 失败,GetLastError() == 0