【发布时间】:2010-12-17 22:39:22
【问题描述】:
我正在制作一个接收 System.Windows.Form 控件并设置其光标的 RAII 类。在析构函数中,它会将光标设置回原来的样子。
但这是个坏主意吗?当此类的对象超出范围时,我可以安全地依赖析构函数将被调用吗?
【问题讨论】:
-
仅供参考,C# 没有与 C++ 相同的析构函数。它确实有终结器,但它们在垃圾收集时被调用,而不是在对象超出范围时调用。
我正在制作一个接收 System.Windows.Form 控件并设置其光标的 RAII 类。在析构函数中,它会将光标设置回原来的样子。
但这是个坏主意吗?当此类的对象超出范围时,我可以安全地依赖析构函数将被调用吗?
【问题讨论】:
这是一个非常非常糟糕的主意。
当变量超出范围时,不调用终结器。它们在对象被垃圾回收之前的某个时间点被调用,这可能是很长时间之后。
相反,你想实现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"))
{
...
}
【讨论】:
SafeHandle 类以解决大多数要求。如果你不需要终结器并且你的类是密封的,那么模式会变得更简单:)
你知道,很多聪明的人说“如果你想在 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 的巨大用途,而是尝试为特殊情况提供特殊构造(lock、using)。出于这个原因,人们尽量不依赖 .NET 中的 RAII,但有时它只是表达配对操作的最自然方式。
finally 很不自然。如果你的 try 中有一个 return 语句退出你的函数,大多数人希望代码立即返回。但它会跳转到 finally 然后返回。同样,如果您正在查看 finally 块,您希望下一行在 finally 之外执行,但在上述场景中,它有时也可以返回。这就像 goto 但有隐藏代码。
using 和 IDisposable 在 .NET 中被赋予了特定用途,但我认为这种限制性约束是完全没有必要的。
编辑:显然埃里克和我在这种用法上存在分歧。 :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
}
【讨论】:
using 肯定更明确,但RAII 在C++ 或(比如)VB6 中是如此成熟的习语,以至于它的使用已成为一种期望,而不是一种意想不到的隐藏动作。如果对象的析构函数不会因为做一些完全不恰当的、未记录的和不可预测的事情而违反最小意外原则,那么它的使用就像using 一样清晰明了,而且不那么冗长(因为every block 是其本地堆栈变量上的隐式 using)
如果您想在 C# 中执行类似 RAII 的操作,请使用 IDisposable 模式
【讨论】: