C# 何时可以在Dispose中调用Finalize?

C# 何时可以在Dispose中调用Finalize?,c#,.net,c++-cli,idisposable,finalizer,C#,.net,C++ Cli,Idisposable,Finalizer,我在浏览Reflector中DLL的反编译源代码时,遇到了以下C代码: 我的第一反应是“什么?我以为你不能手动调用终结器!” 注意:基本类型是对象 为了确保这不是反射器的怪癖,我在ILSpy中打开了这个方法。它生成了类似的代码 我去谷歌确认我的新发现。我找到了的文档,上面说: 派生类型中Finalize的每个实现都必须调用其基类型的Finalize实现这是唯一允许应用程序代码调用Finalize的情况。 现在我不知道该怎么想。这可能是因为DLL是用C++编译的。(注意:我找不到Dispose的实

我在浏览Reflector中DLL的反编译源代码时,遇到了以下C代码:

我的第一反应是“什么?我以为你不能手动调用终结器!”

注意:基本类型是
对象

为了确保这不是反射器的怪癖,我在ILSpy中打开了这个方法。它生成了类似的代码

我去谷歌确认我的新发现。我找到了的文档,上面说:

派生类型中Finalize的每个实现都必须调用其基类型的Finalize实现这是唯一允许应用程序代码调用Finalize的情况。

现在我不知道该怎么想。这可能是因为DLL是用C++编译的。(注意:我找不到Dispose的实现。可能是自动生成的。)这可能是对
IDisposable.Dispose
方法的特殊限制。这可能是两个反编译器中的一个缺陷

一些意见:

  • 我在源代码中找不到Dispose的实现。也许是自动生成的
  • Reflector显示了一个名为
    ~ClassName
    的方法。看起来这个方法实际上可能不是终结器,而是C++析构函数,甚至是普通方法。

这合法吗?如果是,这个案子有什么不同?如果没有,实际发生了什么?在C++/CLI中是否允许,但在C#中不允许?或者这只是反编译器中的一个小故障?

正如其他回答者所指出的,您是对的,处理代码不同的原因是因为它是C++/CLI

C++/CLI使用不同的习惯用法来编写清理代码

  • C#:Dispose()和~ClassName()(终结器)都调用Dispose(bool)。
    • 这三种方法都是由开发人员编写的
  • C++/CLI:Dispose()和Finalize()都调用Dispose(bool),它将调用~ClassName()或!ClassName()(分别是析构函数和终结器)。
    • ~ClassName()和!ClassName()由开发人员编写。
      • 正如您所指出的,~ClassName()的处理方式与C#中的不同。在C++/CLI中,它保持为名为“~ClassName”的方法,而C#中的~ClassName()被编译为
        protectedoverride void Finalize()
    • Dispose()、Finalize()和Dispose(bool)完全由编译器编写。当它这样做时,编译器会做一些通常不应该做的事情
为了演示,这里有一个简单的C++/CLI类:

public ref class TestClass
{
    ~TestClass() { Debug::WriteLine("Disposed"); }
    !TestClass() { Debug::WriteLine("Finalized"); }
};
下面是Reflector反编译为C#语法的输出:

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

我用C++/CLI和C#两种语言编写了测试应用程序,它们定义了父类和子类,子类的构造函数在其中抛出异常。这两个类都有来自其构造函数、dispose方法和终结器的调试输出

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

下面是我用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();
}
输出:

Parent() Child() A first chance exception of type 'System.Exception' occurred in CppCLI-DisposeTest.exe ~Parent() Exception Caught GC::Collect() GC::WaitForPendingFinalizers() !Child() !Parent() GC::Collect() 为了进行比较,这里是C#中的等效程序

以及C#输出:

父项() Child() CSharp-DisposeTest.exe中发生类型为“System.exception”的第一次意外异常 捕获异常 GC::Collect() GC::WaitForPendingFinalizers() ~Child() ~Parent() GC::Collect()
是的,您正在查看C++/CLI代码。除了显式调用finalizer(一种常见的C++/CLI模式)之外,参数上的[Marshallas]属性是一个完全的赠品


C++/CLI的工作原理与C#不同,IDisposable接口和处理模式完全融入了该语言。您从不指定接口名称,也不能直接使用Dispose。一个非常典型的例子是一个包装非托管C++类的REF类包装器。您可以将其粘贴到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”是本机类,包装器将指向它的指针存储为私有成员。构造函数使用新操作符创建实例。这是本机新运算符,托管运算符称为gcnew。~Wrapper()方法声明“析构函数”。这实际上就是dispose方法。编译器生成两个成员,一个是protecteddispose(bool)成员,这个成员是您在代码段中看到的,可能是您熟悉的一次性模式的实现。和Dispose()方法,您也应该看到它。请注意,它会自动调用GC.SuppressFinalize(),就像在C#程序中显式调用一样


这个!Wrapper()成员是终结器,与C#析构函数相同。从析构函数调用它是允许的,而且通常是有意义的。在本例中是这样的。

什么是这样的!类名是指什么?!ClassName是finalize方法。“您从不指定接口名。”但这一行在源代码中:
public ref class ClassName:public System::IDisposable
仅用C++/CLI编写析构函数就足以强制实现接口。试一下我发布的代码片段。出于好奇,C++/CLI是否能够恰当地处理从
IDisposable
基类型派生的类型的构造函数引发异常的场景?我对vb.net和C#的一个主要不满是,在这种情况下设计一个类来避免泄漏是极其困难的。@supercat:我不确定,所以我测试了它。看起来C++/CLI编译器将子组件包装为 Parent() Child() A first chance exception of type 'System.Exception' occurred in CppCLI-DisposeTest.exe ~Parent() Exception Caught GC::Collect() GC::WaitForPendingFinalizers() !Child() !Parent() GC::Collect()
public Child()
{
    try
    {
        Debug.WriteLine("Child()");
        throw new Exception();
    }
    fault
    {
        base.Dispose(true);
    }
}
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;
}
Parent() Child() A first chance exception of type 'System.Exception' occurred in CSharp-DisposeTest.exe Exception Caught GC::Collect() GC::WaitForPendingFinalizers() ~Child() ~Parent() GC::Collect()
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; }
};