何时可以在Dispose中调用Finalize?

9

我正在使用Reflector浏览一个DLL的反编译源代码,然后我发现了这段C#代码:

protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
{
    if (flag1)
    {
        this.~ClassName();
    }
    else
    {
        base.Finalize();
    }
}

我的第一反应是:“什么?我以为你不能手动调用终结器!”
注意:基类型为object
为了确保这不是Reflector的怪癖,我打开了ILSpy中的方法。它生成了类似的代码。
我去谷歌确认了我的新发现。我找到了Object.Finalize的文档,它说:
每个派生类型中Finalize的实现都必须调用其基类型的Finalize实现。这是应用程序代码允许调用Finalize的唯一情况。
现在我不知道该怎么想了。可能是因为DLL是用C++编译的。(注意:我找不到Dispose的实现。也许是自动生成的。)可能是对IDisposable.Dispose方法的特殊允许。也可能是两个反编译器都有缺陷。
一些观察结果:
  • 我在源代码中找不到Dispose的实现,也许它是自动生成的。
  • Reflector显示一个名为~ClassName的方法。看起来这个方法可能并不是最终器,而是C++析构函数,甚至是普通方法。

这是合法的C#吗?如果是,这种情况有什么不同?如果不是,实际上发生了什么?它在C++/CLI中被允许,但在C#中不被允许吗?还是只是反编译器的故障?


!ClassName 是什么意思? - Kendall Frey
!ClassName 是 finalize 方法。 - Omar
2个回答

5
正如其他回答者所指出的,您是正确的,处理代码不同的原因是因为它是C++/CLI。
C++/CLI使用不同的风格来编写清理代码。
C#:Dispose()和~ClassName()(终结器)都调用Dispose(bool)。 所有三种方法都由开发人员编写。
C++/CLI:Dispose()和Finalize()都调用Dispose(bool),它将调用~ClassName()或!ClassName()(析构函数和终结器)。 ~ClassName()和!ClassName()都是由开发人员编写的。正如您所指出的,在C#中,~ClassName()被编译为protected override void Finalize(),而在C++/CLI中,它仍然是一个名为“~ClassName”的方法。
Dispose()、Finalize()和Dispose(bool)都是由编译器独立编写的。在此过程中,编译器做了一些您通常不应该做的事情。
为了演示,这里有一个简单的C++/CLI类:
public ref class TestClass
{
    ~TestClass() { Debug::WriteLine("Disposed"); }
    !TestClass() { Debug::WriteLine("Finalized"); }
};

这是反编译器Reflector输出的结果,将其转换为C#语法:

public class TestClass : IDisposable
{
    private void !TestClass() { Debug.WriteLine("Finalized"); }
    private void ~TestClass() { Debug.WriteLine("Disposed"); }

    public sealed override void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    [HandleProcessCorruptedStateExceptions]
    protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool disposing)
    {
        if (disposing)
        {
            this.~TestClass();
        }
        else
        {
            try
            {
                this.!TestClass();
            }
            finally
            {
                base.Finalize();
            }
        }
    }

    protected override void Finalize()
    {
        this.Dispose(false);
    }
}

编辑

看起来C++/CLI比C#更好地处理构造函数异常。

我在C++/CLI和C#中编写了测试应用程序,定义了一个Parent类和一个Child类,其中Child类的构造函数抛出异常。两个类都有来自它们的构造函数、dispose方法和finalizer的调试输出。

在C++/CLI中,编译器将子构造函数的内容包装在try/fault块中,并在fault中调用父类的Dispose方法。(我相信当异常被某些其他try/catch块捕获时,fault代码会被执行,而不是在catch或finally块中,在这种情况下,它会在向上移动堆栈之前立即执行。但我可能忽略了一些微妙之处。)在C#中,没有隐式的catch或fault块,因此Parent.Dispose()从未被调用。当GC开始收集对象时,两种语言都会调用子类和父类的finalizers。

这是我在C++/CLI中编译的一个测试应用程序:

public ref class Parent
{
public:
    Parent() { Debug::WriteLine("Parent()"); }
    ~Parent() { Debug::WriteLine("~Parent()"); }
    !Parent() { Debug::WriteLine("!Parent()"); }
};

public ref class Child : public Parent
{
public:
    Child() { Debug::WriteLine("Child()"); throw gcnew Exception(); }
    ~Child() { Debug::WriteLine("~Child()"); }
    !Child() { Debug::WriteLine("!Child()"); }
};

try
{
    Object^ o = gcnew Child();
}
catch(Exception^ e)
{
    Debug::WriteLine("Exception Caught");
    Debug::WriteLine("GC::Collect()");
    GC::Collect();
    Debug::WriteLine("GC::WaitForPendingFinalizers()");
    GC::WaitForPendingFinalizers();
    Debug::WriteLine("GC::Collect()");
    GC::Collect();
}

输出:

父类()
子类()
CppCLI-DisposeTest.exe 中出现了一个类型为 'System.Exception' 的首次机会异常
~父类()
捕获到异常
GC::Collect()
GC::WaitForPendingFinalizers()
!子类()
!父类()
GC::Collect()

从 Reflector 输出中可以看出,以下是 C++/CLI 编译器如何编译子类构造函数(反编译成 C# 语法)。

public Child()
{
    try
    {
        Debug.WriteLine("Child()");
        throw new Exception();
    }
    fault
    {
        base.Dispose(true);
    }
}

为了比较,这是C#中相应的程序。

public class Parent : IDisposable
{
    public Parent() { Debug.WriteLine("Parent()"); }
    public virtual void Dispose() { Debug.WriteLine("Parent.Dispose()"); }
    ~Parent() { Debug.WriteLine("~Parent()"); }
}

public class Child : Parent
{
    public Child() { Debug.WriteLine("Child()"); throw new Exception(); }
    public override void Dispose() { Debug.WriteLine("Child.Dispose()"); }
    ~Child() { Debug.WriteLine("~Child()"); }
}

try
{
    Object o = new Child();
}
catch (Exception e)
{
    Debug.WriteLine("Exception Caught");
    Debug.WriteLine("GC::Collect()");
    GC.Collect();
    Debug.WriteLine("GC::WaitForPendingFinalizers()");
    GC.WaitForPendingFinalizers();
    Debug.WriteLine("GC::Collect()");
    GC.Collect();
    return;
}

以下是 C# 的输出结果:

Parent()
Child()
CSharp-DisposeTest.exe 中发生了类型为 'System.Exception' 的第一次机会异常。
捕获到异常
GC::Collect()
GC::WaitForPendingFinalizers()
~Child()
~Parent()
GC::Collect()

出于好奇,C++/CLI是否能够很好地处理从“IDisposable”基类型派生的类型的构造函数抛出异常的情况? 我对vb.net和C#的主要不满之一是,在这种情况下设计一个避免泄漏的类非常困难。 - supercat
@supercat:我不确定,所以我测试了一下。看起来C++/CLI编译器会将子类构造函数包装在try-fault块中,并调用父类的dispose方法。但是,如果您正在编写父类和子类,您也可以做同样的事情,在子类中捕获异常,调用父类的dispose,然后重新抛出异常。请参见编辑。 - David Yaw
谢谢提供信息。这似乎是C ++ / CLI的一个相当明显的优势。不幸的是,我怀疑只有在使用C ++ / CLI编写子类时才能起到这样的保护作用,而如果一个用C ++ / CLI编写的类被其他语言中的类继承,则无法实现此保护。对我来说,如果一个类有一个“IDisposable”字段,在构造完成后永远不会被写入,那么用一行代码处理声明、初始化和清理要比在代码的三个不同部分处理这些事情更加简洁。在vb.net中,可以使用字段初始化器... - supercat
不使用线程静态变量获取构造函数参数的值(让派生类构造函数将它们传递给基类构造函数,然后可以将它们存储在基类字段中,派生类字段初始化器可以使用它们)。如果调用构造函数的代码传入类似于 List<IDisposable> 的东西,则字段初始化器表达式可以使用它来构建需要清理的事物列表。如果构造失败,调用构造函数的代码将必须注意列表中的项目,但它将拥有所需的信息。 - supercat
我发现很讽刺的是,C#对finalize有特殊处理(如果允许代码简单地覆盖Object.Finalize(),则可以在没有这些功能的情况下很好地处理),但却使得对象难以使用具有生命周期绑定的IDisposable对象而不会有泄漏的风险。 - supercat

1

是的,您正在查看C++/CLI代码。除了显式调用终结器的通用C++/CLI模式外,参数上的[MarshalAs]属性是一个明显的提示。

C++/CLI与C#不同,IDisposable接口和disposing模式完全融入语言中。您永远不需要指定接口名称,也不能直接使用Dispose。一个非常典型的例子是ref类包装器,它包装了一个非托管的C++类。您可以将其粘贴到C++/CLI类库中,并查看从此代码生成的IL:

using namespace System;

#pragma managed(push, off)
class Example {};
#pragma managed(pop)

public ref class Wrapper {
private:
    Example* native;
public:
    Wrapper() : native(new Example) {}
    ~Wrapper() { this->!Wrapper(); }
    !Wrapper() { delete native; native = nullptr; }
};

"Example"是本地类,包装器将其作为私有成员存储指针。构造函数使用new运算符创建实例。本地的new运算符称为托管的gcnew。~Wrapper()方法声明“析构函数”,实际上是dispose方法。编译器生成两个成员,一个受保护的Dispose(bool)成员,在您的片段中查看它,可能熟悉它作为可处理模式的实现。还有一个Dispose()方法,您也应该看到它。请注意,它会自动调用GC.SuppressFinalize(),就像您在C#程序中明确执行的那样。
!Wrapper()成员是终结器,与C#析构函数相同。从析构函数中调用它是允许的,并且通常很有意义。在这个例子中确实如此。

你从未指定接口名称。但是这一行在源代码中:public ref class ClassName: public System::IDisposable - Kendall Frey
只需在C++/CLI中编写一个析构函数就足以强制实现接口。请尝试使用我发布的代码片段。 - Hans Passant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接