RAII在C#中是否可以安全使用?和其他垃圾收集语言?

Net*_*zen 10 c# raii

我正在制作一个RAII类,它接受一个System.Windows.Form控件,并设置它的光标.在析构函数中,它将光标设置回原来的状态.

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

Jon*_*eet 20

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

当变量超出范围时,不会调用终结器.它们在对象被垃圾收集之前的某个时刻被调用,这可能是很长一段时间之后.

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

using (YourClass yc = new YourClass())
{
    // Use yc in here
}
Run Code Online (Sandbox Code Playgroud)

那将Dispose自动调用.

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

请注意,您只需要任何的这个时候,你必须要清理资源-大多数类在.NET 落实IDisposable.

编辑:只是约嵌套评论作出回应,我同意它可以是一个有点难看,但你不应该需要using陈述非常经常在我的经验.你可以像这样垂直嵌套使用,如果你有两个直接相邻的:

using (TextReader reader = File.OpenText("file1.txt"))
using (TextWriter writer = File.CreateText("file2.txt"))
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢,我希望'使用'有一个更清晰的语法.它用额外的嵌套来填充我的代码:( (4认同)
  • Re:终结者:通常模式是让处理器和终结器做同样的事情,处理器抑制终结器.您可以在任意数量的位置找到此模式的标准实现. (2认同)
  • Eric当然是绝对正确的,但我要强调的是,现在需要你自己的.NET终结器是非常罕见的.查看`SafeHandle`类来解决大多数需求.如果你不需要一个终结器并且你的课程是密封的,那么这种模式会变得更加简单:) (2认同)

Eri*_*ert 12

你知道,很多聪明的人说"如果你想在C#中实现RAII,就使用IDisposable",而我只是不买它.我知道我在这里是少数,但是当我看到"使用(blah){foo(blah);}"时,我自动认为"blah包含一个非托管资源,需要在foo完成后立即进行清理(或投掷)以便其他人可以使用该资源".我不认为"blah不包含任何有趣的内容,但是需要发生一些语义上重要的突变,我们将通过字符'}'表示语义上重要的操作" - 某些突变就像某些堆栈必须弹出或某些标志必须重置或其他.

我说如果你有一个语义上重要的操作,必须在某些事情完成时完成,我们有一个单词,那个单词是"finally".如果操作很重要,那么它应该表示为一个语句,你可以在那里看到并设置一个断点,而不是一个右大括号的隐形副作用.

那么让我们考虑一下你的特定操作.你想代表:

var oldPosition = form.CursorPosition;
form.CursorPosition = newPosition;
blah;
blah;
blah;
form.CursorPosition = oldPosition;
Run Code Online (Sandbox Code Playgroud)

那段代码非常清楚.所有副作用都在那里,对于想要了解代码正在做什么的维护程序员来说是可见的.

现在你有一个决策点.怎么回事呢?如果blah抛出然后发生意外的事情.你现在不知道"形式"是什么状态; 它可能是"形式"中的代码.它可能是一些突变的中途,现在处于一些完全疯狂的状态.

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

1)将问题提交给其他人.没做什么.希望调用堆栈中的其他人知道如何处理这种情况.表格已经处于不良状态的原因; 光标不在正确位置的事实是它最不担心的事实.不要盯着已经如此脆弱的东西,据报道曾经有过例外.

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

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

坦率地说,我的信念是,正确答案几乎总是(1).如果出现了可怕的问题,那么你无法安全地推断脆弱国家的进一步突变是合法的.如果您不知道如何处理异常,那么放弃.

(2)是带有使用块的RAII给你的解决方案.同样,我们首先使用块的原因是在不再使用块时积极清理重要资源.无论是否发生异常,都需要快速清理这些资源,这就是"使用"块具有"最终"语义的原因.但"最终"语义不一定适用于非资源清理操作的操作; 正如我们所看到的,程序语义 - 负载最终阻止隐含地假设清理总是安全的; 但我们处于异常处理状态的事实表明它可能不安全.

(3)听起来很多工作.

所以总之,我说停止尝试这么聪明.你想改变光标,做一些工作,并取消突变光标.所以写完三行代码就完成了.没有华丽的裤子RAII是必要的; 它只是增加了不必要的间接性,它使得阅读程序变得更加困难,并且它使得它在特殊情况下可能脆,而不是更脆弱.

  • `finally`很混乱.人们想要的是一种表达RAII的简洁方式.C#/ .NET的设计并没有考虑到RAII的巨大用处,而是试图为特殊情况提供特殊的结构(`lock`,`using`).出于这个原因,人们尽量不依赖于.NET中的RAII,但有时它只是表达配对操作的最自然的方式. (7认同)
  • 状态改变RAII的问题在于它恰恰相反:它需要语义上的关注并将其隐藏在资源分配/解除分配机制中,而不是它应该存在的位置.这些机制有一个目的:顺利处理资源管理,以便您可以继续使用程序的业务逻辑.将它们用于预期目的; 不要将机制逻辑与业务逻辑混合在一起,也不要在分配机制中隐藏业务逻辑. (3认同)
  • 我同意@Konrad.而且,我发现'终于'非常不自然.如果你的try中有一个退出语句退出你的函数,大多数人都希望代码立即返回.但它会跳转到最后然后返回.同样,如果您正在查看finally块,您希望下一行在finally之外执行,但在上述场景中,它有时也可以返回.这就像转到但隐藏的代码. (2认同)
  • 最好将'RAII'中的'R'视为'Responsiblity'.因此,无论何时获得责任,例如重置游标状态的责任,您都要将该责任移交给将正确处理它的对象.我认为这里没有令人信服的论据,在机制逻辑和业务逻辑之间做出区分是个好主意.定义和使用对象来处理诸如重置光标状态之类的东西比一堆复制粘贴的finally块更清晰,更易于维护. (2认同)

Sam*_*ell 6

编辑:显然,Eric和我对这种用法存在分歧.:○

你可以使用这样的东西:

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
}
Run Code Online (Sandbox Code Playgroud)

  • 从Eric的回复评论:"另一方面,使用声明比RIAA清晰得多" - 我不同意.虽然`using`肯定更加明确*,但RAII在C++或(比方说)VB6中是如此成熟的习惯用法,它的使用已成为一种期望,而不是意外的隐藏动作.如果对象的析构函数没有通过做一些完全不合适,无法记录和不可预测的事情来违反最小惊喜的原则,那么它的使用就像`using`一样清晰和干净,并且不那么冗长(因为*每个*块都是隐含的)在其本地堆栈变量上使用` (4认同)