C# 基本程序中的析构函数不起作用(输出丢失)

2 c# constructor garbage-collection destructor

我写了下面非常基本的程序,我是 C# 新手。析构函数 ~Program() 不会被调用,因此我在输出中没有看到“析构函数调用”字符串。我检查过其他类似的问题,但没有找到我的答案。谢谢。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static System.Console;

namespace LyfeCicleObject
{
    class Program
    {
        public Program()
        {
            WriteLine("Cons called");
        }

        ~Program()
        {
            WriteLine("Destructor called");           
        }

        static void Main(string[] args)
        {
            WriteLine("Main started");

            Program p1 = new Program();
            {
                WriteLine("Block started");
                Program p2 = new Program();
                WriteLine("Block ended");
            }

            WriteLine("Main ended");
        }

        }

    }
Run Code Online (Sandbox Code Playgroud)

mad*_*ion 6

简短的答案 - 你没有看到“析构函数调用”输出的原因 - 被埋在评论中的某个地方:

.NET Core 不会在程序结束时运行终结器

(请参阅:终结器(C# 编程指南))。

.NET Framework 会尝试执行此操作,但 .NET Core 不会执行此操作。

免责声明:我们无法知道这些陈述是否继续成立;这就是目前它们的实施和记录方式。

不过,根据 Raymond Chen 在他的文章《每个人都以错误的方式思考垃圾收集》中所说,如果 .NET Framework 在程序结束时没有运行终结器,它也不会无效。相关引用从不同的角度说是这样的:

正确编写的程序不能假设终结器将会运行。

因此,只要您不假设终结器会运行,它们的实现方式或实现是否发生更改都无关紧要。

在进一步使用 C# 之前,您必须放弃 .NET 中析构函数的想法,因为它们根本不存在。C# 使用 C++ 的析构函数语法作为终结器,但相似之处仅此而已。

好消息是,有一种方法可以做一些接近您想要做的事情,但这需要范式转变,即您对资源获取和释放的看法发生重大变化。你是否真的需要这样做是一个完全不同的问题。

终结器并不是释放需要及时释放的资源的唯一方法,甚至不是最好的方法。我们有一次性模式来帮助解决这个问题。

一次性模式允许类实现者选择一种通用机制来确定性地释放资源(不包括托管堆上的内存)。它包含终结器,但仅作为对象未正确处理时清理的最后机会,特别是在进程未终止的情况下。

我想说与 C++ 析构函数相比,您会看到的主要区别是:

  1. 该类的实现者还必须做更多的事情来支持一次性模式。
  2. 该类的消费者还必须通过声明选择加入该类using

您不会看到的是,内存不一定会立即回收。


如果您想了解更多有关如何操作的信息,请继续阅读...

在进入任何代码之前,值得一提的是一些注意事项:

  • 终结器永远不应该为空。它使实例的存活时间更长,而且没有任何效果。
  • 正如 mjwills 在评论中所说,99.9% 的情况下,你不应该编写终结器。如果您发现自己在编写代码,请退后一步,确保您有充分的理由使用 .NET 代码来执行此操作,而不是因为您会在 C++ 中这样做。
  • 通常,您会Dispose(bool)在从实现一次性模式的类派生之后进行重写,而不是创建需要一次性的类层次结构的基础。例如,.Designer.csWindows 窗体应用程序中的文件将进行覆盖Dispose(bool),以便components在该字段不是null.

好吧,代码...

以下是实现一次性模式的简单类的示例。它不提供对子类的支持,因此它被标记sealed为。Dispose(bool)private

public sealed class SimpleDisposable : IDisposable
{
    public SimpleDisposable()
    {
        // acquire resources
    }

    ~SimpleDisposable()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        // Suppress calling the finalizer because resources will have been released by then.
        GC.SuppressFinalize(this);
        Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // release managed resources
            // (you don't want to do this when calling from the finalizer because the GC may have already finalized and collected them)
        }

        // release unmanaged resources
    }
}
Run Code Online (Sandbox Code Playgroud)

实际的清理发生在该Dispose(bool)方法中。如果参数为true,则意味着通过IDisposable接口(通常是using语句但不一定)进行处置,并且也可以清理托管资源。如果是false,则意味着处置是作为 GC 扫描的一部分进行的,因此您无法触及托管资源,因为它们可能已被收集。

如果您正在编写需要支持一次性模式的基类,则情况会略有变化:Dispose(bool)变得如此protectedvirtual因此它可以被子类覆盖,但消费者仍然无法访问。

以下是支持子类一次性模式的基类示例。

public abstract class BaseDisposable : IDisposable
{
    protected BaseDisposable()
    {
        // acquire resources
    }

    ~BaseDisposable()
    {
        Dispose(false);
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // release managed resources
        }

        // release unmanaged resoures
    }
}
Run Code Online (Sandbox Code Playgroud)

下面是使用该支持的子类。子类也不需要实现终结器或IDisposable.Dispose. 他们所要做的就是 override Dispose(bool),处理自己的资源,然后调用基本实现。

public class DerivedDisposable : BaseDisposable
{
    public DerivedDisposable()
    {
        // acquire resources for DerivedDisposable
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // release DerivedDisposable's managed resources
        }

        // release DerivedDisposable's unmanaged resources

        // Let the base class do its thing
        base.Dispose(disposing);
    }
}
Run Code Online (Sandbox Code Playgroud)

那么处置托管和非托管资源意味着什么?

托管资源类似于其他一次性对象甚至非一次性对象(例如字符串)。BCL 中的某些一次性类型会将此类字段设置为,null以确保 GC 不会找到对它们的活动引用。

当您的类被处置时,消费者决定不再需要它及其资源。如果您的对象包含其他一次性物品,则可以处置这些对象,依此类推,因为它不会在垃圾收集期间发生。

非托管资源是诸如文件句柄、全局内存、内核对象之类的东西......几乎是您通过调用操作系统分配的任何东西。这些不受垃圾收集器的影响,无论如何都需要释放,因此它们不受测试disposing

如果您的一次性对象使用了另一个具有非托管资源的一次性对象,则该对象有责任实现 Disposable 模式以释放其资源,并且您有责任使用它。

并非所有实现的对象IDisposable实际上都具有非托管资源。通常,基类支持一次性模式只是因为其作者知道至少有一个从它派生的类可能需要使用非托管资源。但是,如果一个类没有实现一次性模式,则其子类之一可以在需要时引入该支持。


让我们稍微改变一下你的程序,让它做你所期望的事情,但现在使用一次性模式。

注意:据我所知,创建Main包含类的实例并不常见Program。我在这里这样做是为了使内容尽可能接近原始内容。

using System;

internal sealed class Program : IDisposable
{
    private readonly string _instanceName;

    public Program(string instanceName)
    {
        _instanceName = instanceName;

        Console.WriteLine($"Initializing the '{_instanceName}' instance");
    }

    ~Program()
    {
        Dispose(false);
    }

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

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            Console.WriteLine($"Releasing the '{_instanceName}' instance's managed resources");
        }

        Console.WriteLine($"Releasing the '{_instanceName}' instance's unmanaged resources");
    }

    private static void Main(string[] args)
    {
        Console.WriteLine("Main started");

        Program p0 = new Program(nameof(p0));
        using (Program p1 = new Program(nameof(p1)))
        {
            Console.WriteLine("Outer using block started");
            using (Program p2 = new Program(nameof(p2)))
            {
                Console.WriteLine("Inner using block started");
                Console.WriteLine("Inner using block ended");
            }
            Console.WriteLine("Outer using block ended");
        }

        Console.WriteLine("Main ended");
    }
}
Run Code Online (Sandbox Code Playgroud)

针对 .NET Framework 4.7.2 构建并运行,您将获得以下输出:

public sealed class SimpleDisposable : IDisposable
{
    public SimpleDisposable()
    {
        // acquire resources
    }

    ~SimpleDisposable()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        // Suppress calling the finalizer because resources will have been released by then.
        GC.SuppressFinalize(this);
        Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // release managed resources
            // (you don't want to do this when calling from the finalizer because the GC may have already finalized and collected them)
        }

        // release unmanaged resources
    }
}
Run Code Online (Sandbox Code Playgroud)

针对 .NET Core 2.1 构建并运行,您将获得以下输出:

public abstract class BaseDisposable : IDisposable
{
    protected BaseDisposable()
    {
        // acquire resources
    }

    ~BaseDisposable()
    {
        Dispose(false);
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // release managed resources
        }

        // release unmanaged resoures
    }
}
Run Code Online (Sandbox Code Playgroud)

由于语句的原因,实例p1和实例p2按照与构造它们相反的顺序进行处置using,并且托管和非托管资源都被释放。这是尝试使用“析构函数”背后所需的行为。

另一方面,.NET Framework 和 .NET Core 最后做了一些不同的事情,显示了我在开头提到的差异:

  • .NET Framework 的 GC 调用了终结器,p0因此它只释放非托管资源。
  • .NET Core 的 GC 没有调用 .NET Core 的终结器p0