【问题标题】:C# destructors and GC not really solving problems compared to C++ destructors与 C++ 析构函数相比,C# 析构函数和 GC 并不能真正解决问题
【发布时间】:2017-06-22 04:27:13
【问题描述】:

编辑:这个问题旨在比较 C# 和 C++ 实现。讨论时无需戏剧化。无论如何,我个人更喜欢用 C# 开发。

假设我有以下课程:

class Foo {
            FileStream f;
            public Foo() {
                f = File.Open("somefile.txt", FileMode.Open);
            }
            ~Foo() {
                f.Close();
            }
        }

然后我在 Main 中调用以下内容:

static void Main(){
    doSomething();
    // this one creates an exception, file in use
    doSomething();
}
static void doSomething(){
    Foo f = new Foo();
}

在 C++ 中,这段代码可以正常工作:

    class Foo {
public:
    std::ofstream ofs;
    Foo() { ofs = ofstream("somefile.txt"); }
    ~Foo() { ofs.close(); }
};

void doSomething() {
    Foo f();
}

int main() {
    doSomething();
    doSomething();
    return 0;
}

当第一个doSomething() 超出范围时,第一个对象调用其析构函数并关闭文件。

在 C# 中,这总是会抛出一个异常,说明该文件正在被另一个进程使用。显然没有调用析构函数。

参考 MSDN 关于析构函数的页面here,它会使阅读它的人感到困惑,并让他们认为这实际上是一个析构函数。虽然它实际上是 GC 决定调用它时由 GC 调用的终结器,但我无法控制。

是的,我可以实现IDisposeable,但我不希望每个使用我的库的程序员都记得调用Dispose(),否则他会遇到未处理的异常。

我可以在两个doSomething() 方法之间调用GC.Collect() 并且不会抛出异常。再一次,这将类似于Dispose(),图书馆的用户必须记住它。没有提到来自 MS 的无数警告告诉人们请不要使用GC.Collect()

所以我的问题是,如果 MS 想要像 C++ 那样拥有析构函数,他们是否只需要在每次方法超出范围时调用垃圾收集器?还是对他们来说会更复杂?这样的设计会有什么样的复杂性?

另外,如果很简单,他们为什么不做呢?

此外,有没有办法强制 C# 中的析构函数在超出范围时被调用?

编辑:

我正在查看 C++/CLI 实现,有趣的是,关键字 gcnew 恰好允许我希望 C# 发生的事情。所以答案就在那个实现中。在他们的文档中,当托管对象超出范围时,将调用 GC 并调用托管对象的析构函数。这并不强制用户手动Dispose 对象。

这回答了一个问题,即当对象超出范围时,MS 使 GC 以类似方式工作有多“难”。它只是自动调用GC.Collect(),这会强制 GC 调用析构函数,类似于 C++ 中发生的情况。

【问题讨论】:

  • 如果你想知道他们为什么做某事,你需要问微软他们为什么做某事。你在这里得到的只是意见。我会说,将 C# 析构函数视为 C++ 析构函数是完全错误的。它们完全不同。 C# 没有堆栈分配的类,因此“超出范围”的想法不适用。唯一重要的是根据 GC 的规则,堆分配的对象是否可以访问。
  • 不同的语言,不同的行为。垃圾收集器的非确定性行为(你不知道它什么时候会运行,只是它会运行)在 RAII 上扮演着邪恶的地狱,所以如果你想在给定时间释放资源,你该死的必须做它自己而不是指望析构函数。从好的方面来说,大多数内存管理都是一劳永逸的。权衡。
  • 您可能想阅读 Eric Lippert 的关于 C# 中终结器的博文ericlippert.com/2015/05/18/…
  • 还有Raymond Chen's piece on garbage collectors。本来想写答案的,为什么要这么麻烦?陈先生已经做到了。
  • “对于 MS 来说,实现类似的析构函数会有多复杂”——本身并不复杂,但与他们的整个设计目标背道而驰。 “我可以实现在超出范围时自动调用的析构函数”——关于学习从真正的 RAII 语言到类似 C# 语言所需的习语,已经有广泛的讨论。 C# 终结器是处理错误代码的备份;您的代码设计首先不应该依赖它们,因此它们何时执行对您来说应该无关紧要。正确编写您的代码,这并不重要。

标签: c# c++ garbage-collection c++-cli destructor


【解决方案1】:

正如您所推测的,C# 没有确定性析构函数,将其与 C++ 进行比较总是“苹果对橘子”。此外,在 C# 类上实现析构函数将导致该类实例的 GC 清理更加不确定,因为 GC 会将任何具有析构函数的实例放入列表中,稍后由单独的线程处理。

如果您想要确定性地处理实例,请实现IDisposable 接口/模式。这就是它的用途。

【讨论】:

  • 是的,我明白这一点,但我不想强迫图书馆的用户在我的实例上使用Dispose(),否则他们会有错误和错误
  • @AmmarSalman 你可以两者兼得。然后用户可以立即进行清理,否则他们只需要处理 GC 的意愿。
  • 明白。迫使您的班级的用户关心生命周期管理的唯一原因是他们可能希望在其生命周期内对其执行多项操作 - 在这种情况下,这是用户的责任,因为只有他们知道何时完成。如果每个方法都是原子的,您只需要在方法内处理创建和处置。也许通过创建一个单独的内部或私有类来完成实际工作。
【解决方案2】:

这不是 C# 问题。您的班级应该正确处理打开和关闭。

public static class Foo
{
    public static void DoSomething()
    {
        string someText = "someText";
        using (StreamWriter writer = new StreamWriter("myfile.txt"))
        {
            writer.Write(someText);
            writer.Close();
            writer.Dispose();
        }
    }
}

【讨论】:

  • 这与我的要求相去甚远。真的很遥远。我只是在课堂上做一个文件打开的例子。我可以有一个数据库连接,我在对象的生命周期内不断调用连接上的东西,然后在我的对象死后我希望数据库关闭。如果我的对象未在using 中处理或使用,则连接不会关闭。在 C++ 中,一旦对象超出范围,将调用析构函数并自动关闭连接
  • 你使用using的唯一地方是结合局部变量和局部作用域。与 C++ 的析构函数自动调用相比。 using 语句将处理有问题的对象 - 递归地,因为 Dispose 方法处理对象的成员。与 C++ 中成员的自动销毁相比。当然,这需要您注意并确保正确实施每个 Dispose 方法,并且您可以在任何地方使用 using
  • @Everyone,对于数据库连接,使用它的正确方法是“打开连接,执行查询,处理它”::Net 框架处理连接池,并注意不必每次都创建到 db 的物理连接。如果您想使用后者,您真的需要尝试停止思考 c++ 方式并开始思考 c# 方式。正如其他人指出的那样,两者完全不同,需要不同的做事方式。
  • @GianPaolo 看C++/CLI 的实现,在销毁对象的方式上真的很像C++。当它超出范围时,会自动调用GC.Collect,因此调用析构函数不需要用户手动调用Dispose。这就是我真正想要的。
  • @Everyone 为什么你反对调用 Dispose 的必要性?如果有人从 C# 环境来到 c++,一直说:“我不希望用户在使用 new 创建实例后必须调用 delete。在 C# 中我不需要它,为什么在 c++ 中我必须这样做?” .简化答案:这就是 c++ 的工作原理,如果你想让你的生活更轻松,请使用智能指针”。现在颠倒范式,并使用正确的工具在 C# 中执行此操作:创建 IDisposable 类,您的用户将知道他们必须在正确的时间。using 会在那里提供帮助。
猜你喜欢
  • 2012-03-05
  • 2010-10-28
  • 2016-09-29
  • 2018-08-29
  • 2018-09-08
  • 2013-12-14
  • 2011-10-06
  • 2011-04-03
  • 2010-12-16
相关资源
最近更新 更多