10

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

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

4

4 回答 4

21

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

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

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

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

那会Dispose自动调用。

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

请注意,只有在需要清理资源时才需要这些——.NET 中的大多数类都没有实现IDisposable.

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

using (TextReader reader = File.OpenText("file1.txt"))
using (TextWriter writer = File.CreateText("file2.txt"))
{
    ...
}
于 2009-11-18T16:10:33.250 回答
13

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

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

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

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

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

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

于 2009-11-18T16:41:29.157 回答
6

编辑:显然埃里克和我在这种用法上存在分歧。: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
}
于 2009-11-18T17:02:48.923 回答
1

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

于 2009-11-18T16:10:40.480 回答