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# 中不允许?或者它只是反编译器中的一个小故障?

4

2 回答 2

5

正如其他回答者所指出的那样,您是对的,处置代码不同的原因是因为它是 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() 被编译为protected override void Finalize().
    • 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 方法和终结器的调试输出。

在 C++/CLI 中,编译器将子构造函数的内容包装在 try/fault 块中,并在错误时调用父构造函数的 Dispose 方法。(我相信当异常被其他一些 try/catch 块捕获时执行错误代码,而不是在向上移动堆栈之前立即执行的 catch 或 finally 块。但我可能会错过一个微妙之处.) 在 C# 中,没有隐式的 catch 或故障块,因此 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();
}

输出:

父母()
孩子()
CppCLI-DisposeTest.exe 中发生了“System.Exception”类型的第一次机会异常
〜父母()
异常捕获
GC::收集()
GC::WaitForPendingFinalizers()
!孩子()
!父()
GC::收集()

查看 Reflector 输出,以下是 C++/CLI 编译器如何编译 Child 构造函数(反编译为 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# 输出:

父母()
孩子()
CSharp-DisposeTest.exe 中出现了“System.Exception”类型的第一次机会异常
异常捕获
GC::收集()
GC::WaitForPendingFinalizers()
〜孩子()
〜父母()
GC::收集()
于 2012-08-21T00:45:48.150 回答
1

是的,您正在查看 C++/CLI 代码。除了对终结器的显式调用(一种常见的 C++/CLI 模式)之外,参数上的 [MarshalAs] 属性是一个死的赠品。

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

“示例”是本机类,包装器将指向它的指针存储为私有成员。构造函数使用new运算符创建实例。这是本地 new 运算符,托管的称为gcnew。~Wrapper() 方法声明了“析构函数”。这实际上是 dispose 方法。编译器生成两个成员,一个受保护的 Dispose(bool) 成员,这是您在代码片段中查看的成员,您可能熟悉它作为一次性模式的实现。还有一个 Dispose() 方法,您也应该看到它。请注意,它会自动调用 GC.SuppressFinalize(),就像您在 C# 程序中显式执行的操作一样。

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

于 2012-08-21T00:50:41.597 回答