【问题标题】:Is RAII safe to use in C#? And other garbage collecting languages?在 C# 中使用 RAII 是否安全?和其他垃圾收集语言?
【发布时间】:2010-12-17 22:39:22
【问题描述】:

我正在制作一个接收 System.Windows.Form 控件并设置其光标的 RAII 类。在析构函数中,它会将光标设置回原来的样子。

但这是个坏主意吗?当此类的对象超出范围时,我可以安全地依赖析构函数将被调用吗?

【问题讨论】:

  • 仅供参考,C# 没有与 C++ 相同的析构函数。它确实有终结器,但它们在垃圾收集时被调用,而不是在对象超出范围时调用。

标签: c# raii


【解决方案1】:

这是一个非常非常糟糕的主意。

当变量超出范围时,调用终结器。它们在对象被垃圾回收之前的某个时间点被调用,这可能是很长时间之后。

相反,你想实现IDisposable,然后调用者可以使用:

using (YourClass yc = new YourClass())
{
    // Use yc in here
}

这将自动调用Dispose

终结器在 C# 中很少需要 - 只有当您直接拥有非托管资源(例如 Windows 句柄)时才需要它们。否则,您通常会有一些托管包装类(例如FileStream),如果需要,它会有一个终结器。

请注意,只有在需要清理资源时才需要任何 - .NET 中的大多数类不要实现IDisposable

编辑:只是为了回应关于嵌套的评论,我同意它可能有点难看,但根据我的经验,你不应该经常需要 using 语句非常。你也可以像这样垂直嵌套 using,如果你有两个 直接 相邻的:

using (TextReader reader = File.OpenText("file1.txt"))
using (TextWriter writer = File.CreateText("file2.txt"))
{
    ...
}

【讨论】:

  • 使用 'using' 是否足以保证析构函数将按照下面建议的@JaredPar 调用?如果它确实保证它,我为什么要实现 IDispose 呢?
  • IDispose 我的意思是 IDisposable
  • 谢谢,我希望“使用”有更简洁的语法。它在我的代码中添加了额外的嵌套:(
  • Re: 终结器:通常模式是让处置器和终结器做同样的事情,而处置器抑制终结器。您可以在任意数量的地方找到该模式的标准实现以进行复制。
  • Eric 绝对正确(当然),但我要强调的是,如今在 .NET 中需要您自己的终结器非常非常。查看SafeHandle 类以解决大多数要求。如果你不需要终结器并且你的类是密封的,那么模式会变得更简单:)
【解决方案2】:

你知道,很多聪明的人说“如果你想在 C# 中实现 RAII,就使用 IDisposable”,我就是不买。我知道我在这里是少数,但是当我看到“using(blah) { foo(blah); }”时,我会自动认为“blah 包含一个非托管资源,一旦 foo 完成就需要积极清理(或抛出),以便其他人可以使用该资源”。我不认为“废话不包含任何有趣的内容,但需要发生一些语义上重要的突变,我们将用字符'}'来表示语义上重要的操作”——一些突变,例如必须弹出一些堆栈或必须重置某些标志或其他任何东西。

我说如果你有一个语义上重要的操作必须在某事完成时完成,我们有一个词,这个词是“终于”。如果操作是重要,那么它应该被表示为一个语句,您可以在那里看到并放置一个断点,而不是右花括号的无形副作用。

让我们考虑一下您的特定操作。你想代表:

var oldPosition = form.CursorPosition;
form.CursorPosition = newPosition;
blah;
blah;
blah;
form.CursorPosition = oldPosition;

该代码非常清晰。所有的副作用都在那里,想要了解您的代码在做什么的维护程序员可以看到。

现在您有了一个决策点。如果blah抛出怎么办?如果 blah 抛出,那么 发生了一些意想不到的事情。您现在不知道“形式”处于什么状态;可能是“表单”中的代码抛出。它可能已经经历了一些突变,现在处于某种完全疯狂的状态。

鉴于这种情况,你想做什么?

1) 将问题推给其他人。没做什么。希望调用堆栈上的其他人知道如何处理这种情况。表格已处于不良状态的原因;它的光标不在正确的位置这一事实是它最不担心的。已经很脆弱的东西别戳了,报过一次异常。

2) 在 finally 块中重置光标,然后将问题报告给其他人。希望 - 没有任何证据表明您的希望会实现 - 在您知道处于脆弱状态的表单上重置光标本身不会引发异常。因为,在那种情况下会发生什么?可能有人知道如何处理的原始异常丢失了。您破坏了有关问题最初原因的信息,这些信息可能有用。你已经用其他一些关于光标移动失败的信息替换了它,这对任何人都没有用。

3) 编写处理 (2) 问题的复杂逻辑——捕获异常,然后尝试在单独的 try-catch-finally 块中重置光标位置,该块抑制新异常并重新抛出原始异常例外。这可能很难做到正确,但您可以做到。

坦率地说,我认为正确答案几乎总是 (1)。如果出现了可怕的问题,那么您就无法安全地推断对脆弱状态的进一步突变是合法的。如果您不知道如何处理异常,请放弃。

(2) 是带有 using 块的 RAII 为您提供的解决方案。同样,我们首先使用块的原因是在它们不再可用时积极清理重要资源。 无论是否发生异常,这些资源都需要快速清理,这就是为什么“using”块具有“finally”语义的原因。但是“finally”语义不一定适用于不是资源清理操作的操作;正如我们所见,程序语义加载的 finally 块隐含地假设进行清理总是安全的;但是我们处于异常处理情况的事实表明它可能不安全。

(3) 听起来工作量很大。

所以简而言之,我说不要再这么聪明了。你想改变光标,做一些工作,然后取消光标。所以写三行代码来做这件事,完成。不需要花哨的RAII;它只是增加了不必要的间接性,使程序更难阅读,并且在特殊情况下可能更脆弱,而不是更脆弱。

【讨论】:

  • finally 很混乱。人们想要的是一种简洁的方式来表达 RAII。 C#/.NET 的设计根本没有考虑到 RAII 的巨大用途,而是尝试为特殊情况提供特殊构造(lockusing)。出于这个原因,人们尽量不依赖 .NET 中的 RAII,但有时它只是表达配对操作的最自然方式。
  • 我同意@Konrad。另外,我觉得finally 很不自然。如果你的 try 中有一个 return 语句退出你的函数,大多数人希望代码立即返回。但它会跳转到 finally 然后返回。同样,如果您正在查看 finally 块,您希望下一行在 finally 之外执行,但在上述场景中,它有时也可以返回。这就像 goto 但有隐藏代码。
  • @Eric:你说 RAII 除了资源清理(对“资源”的定义非常狭隘)之外的所有事情都是错误的,因为你太从字面上坚持“RAII”的标签。 RAII 是一个可怕的误称,它只是提供了一种以特定顺序执行操作的极其可靠和简洁的方式。你当然是对的,因为 usingIDisposable 在 .NET 中被赋予了特定用途,但我认为这种限制性约束是完全没有必要的。
  • 状态改变 RAII 的问题在于它的作用正好相反:它需要一个语义问题并将其隐藏在资源分配/释放机制中,而这正是它不应该存在的地方。这些机制有一个目的:顺利处理资源管理,以便您可以继续处理程序的业务逻辑。将它们用于预期目的;不要把机制逻辑和业务逻辑混在一起,不要把业务逻辑隐藏在分配机制中。
  • 最好将“RAII”中的“R”视为“责任”。因此,无论何时获得责任,例如重置光标状态的责任,您都会将该责任移交给能够正确处理它的对象。我认为这里没有令人信服的论点,即区分机制逻辑和业务逻辑是一个好主意。定义和使用对象来处理诸如重置光标状态之类的事情比一堆复制粘贴的 finally 块更清晰、更易于维护。
【解决方案3】:

编辑:显然埃里克和我在这种用法上存在分歧。 :o

你可以这样使用:

public sealed class CursorApplier : IDisposable
{
    private Control _control;
    private Cursor _previous;

    public CursorApplier(Control control, Cursor cursor)
    {
        _control = control;
        _previous = _control.Cursor;
        ApplyCursor(cursor);
    }

    public void Dispose()
    {
        ApplyCursor(_previous);
    }

    private void ApplyCursor(Cursor cursor)
    {
        if (_control.Disposing || _control.IsDisposed)
            return;

        if (_control.InvokeRequired)
            _control.Invoke(new MethodInvoker(() => _control.Cursor = cursor));
        else
            _control.Cursor = cursor;
    }
}

// then...

using (new CursorApplier(control, Cursors.WaitCursor))
{
    // some work here
}

【讨论】:

  • 来自 cmets 对 Eric 的回复:“另一方面,using 语句比 RIAA 干净得多”——我不同意。虽然using 肯定更明确,但RAII 在C++ 或(比如)VB6 中是如此成熟的习语,以至于它的使用已成为一种期望,而不是一种意想不到的隐藏动作。如果对象的析构函数不会因为做一些完全不恰当的、未记录的和不可预测的事情而违反最小意外原则,那么它的使用就像using 一样清晰明了,而且不那么冗长(因为every block 是其本地堆栈变量上的隐式 using
【解决方案4】:

如果您想在 C# 中执行类似 RAII 的操作,请使用 IDisposable 模式

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多