在 C# 中使用 RAII 安全吗?还有其他垃圾收集语言吗?
我正在制作一个 RAII 类,它接受 System.Windows.Form 控件并设置其光标。在析构函数中,它将光标设置回原来的位置。
但这是一个坏主意吗?我可以放心地相信,当此类的对象超出范围时,将调用析构函数吗?
I was making an RAII class that takes in a System.Windows.Form control, and sets its cursor. And in the destructor it sets the cursor back to what it was.
But is this a bad idea? Can I safely rely that the destructor will be called when objects of this class go out of scope?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
这是一个非常非常糟糕的主意。
当变量超出范围时不会调用终结器。它们在对象被垃圾收集之前的某个时刻被调用,这可能是在很长一段时间之后。
相反,您想要实现
IDisposable
,然后调用者可以使用:这将自动调用
Dispose
。在 C# 中很少需要终结器 - 仅当您直接拥有非托管资源(例如 Windows 句柄)时才需要它们。否则,您通常有一些托管包装类(例如
FileStream
),如果需要的话,它将有一个终结器。请注意,只有当您需要清理资源时才需要其中的任何一个 - .NET 中的大多数类不实现
IDisposable
。编辑:只是为了回应有关嵌套的评论,我同意它可能有点难看,但根据我的经验,您不应该经常需要
using
语句非常。如果您有两个直接相邻的使用,您也可以像这样垂直嵌套使用:This is a very, very bad idea.
Finalizers are not called when variables go out of scope. They're called at some point before the object is garbage collected, which could be a long time later.
Instead, you want to implement
IDisposable
, and then callers can use:That will call
Dispose
automatically.Finalizers are very rarely necessary in C# - they're only required when you directly own an unmanaged resource (e.g. a Windows handle). Otherwise, you typically have some managed wrapper class (e.g.
FileStream
) which will have a finalizer if it needs one.Note that you only need any of this when you have resources to be cleaned up - most classes in .NET don't implement
IDisposable
.EDIT: just to respond to the comment about nesting, I agree it can be a bit ugly, but you shouldn't need
using
statements very often in my experience. You can also nest usings vertically like this, in the case where you've got two directly adjacent ones:你知道,很多聪明人都说“如果你想在 C# 中实现 RAII,请使用 IDisposable”,但我并不买账。我知道我在这里属于少数派,但是当我看到“using(blah) { foo(blah); }”时,我自动认为“blah 包含一个非托管资源,需要在 foo 完成后立即清理(或抛出)以便其他人可以使用该资源”。我不认为“blah 包含任何有趣的东西,但需要发生一些语义重要的突变,我们将用字符 '}' 来表示语义上重要的操作” - 一些突变,例如必须弹出一些堆栈或者必须重置一些标志等等。
我说,如果你有一个语义上重要的操作必须在某件事完成时完成,我们有一个词来形容它,这个词就是“最后”。如果该操作重要,那么它应该表示为一条您可以在那里看到并放置断点的语句,而不是右花括号的不可见副作用。
因此,让我们考虑一下您的特定操作。您想要表示:
该代码完全清晰。所有副作用都在那里,对于想要了解代码正在做什么的维护程序员来说是可见的。
现在你有一个决策点。如果 blah 抛出怎么办?如果 blah 抛出,则发生了意想不到的事情。你现在根本不知道“形式”处于什么状态;它可能是“形式”中的代码引发的。它可能已经经历了一些突变,现在处于某种完全疯狂的状态。
鉴于这种情况,你想做什么?
1)把问题转嫁给别人。什么也不做。希望调用堆栈上的其他人知道如何处理这种情况。表单已经处于不良状态的原因;事实上,它的光标不在正确的位置是最不用担心的。已经这么脆弱的东西就别去戳了,有一次就报异常了。
2)在finally块中重置光标,然后将问题报告给其他人。希望——没有任何证据表明您的希望会实现——在您知道处于脆弱状态的表单上重置光标本身不会引发异常。因为,在这种情况下会发生什么?最初的异常(也许有人知道如何处理)丢失了。您毁坏了有关问题原始原因的信息,这些信息可能有用。并且您已将其替换为有关光标移动失败的其他一些信息,这些信息对任何人都没有用。
3)编写复杂的逻辑来处理(2)的问题——捕获异常,然后尝试在单独的try-catch-finally块中重置光标位置,该块抑制新异常并重新抛出原始异常。要做到这一点可能很困难,但你可以做到。
坦率地说,我相信正确答案几乎总是 (1)。如果出现了可怕的问题,那么你就无法安全地推断出对脆弱状态的进一步突变是合法的。如果您不知道如何处理异常,请放弃。
(2) 是带有 using 块的 RAII 为您提供的解决方案。同样,我们首先使用块的原因是在重要资源无法再使用时积极清理它们。 无论是否发生异常,这些资源都需要快速清理,这就是为什么“using”块具有“finally”语义的原因。但“最终”语义不一定适合非资源清理操作;正如我们所看到的,充满程序语义的finally 块隐式地假设进行清理总是安全的;但我们处于异常处理情况的事实表明它可能不安全。
(3) 听起来工作量很大。
简而言之,我说不要再试图变得如此聪明。您想要改变光标,做一些工作,然后取消改变光标。因此,编写三行代码即可完成此操作。不需要花哨的 RAII;它只是增加了不必要的间接性,使程序变得更难阅读,而且在特殊情况下它可能会变得更脆弱,而不是更脆弱。
You know, a lot of smart people say "use IDisposable if you want to implement RAII in C#", and I just don't buy it. I know I'm in the minority here, but when I see "using(blah) { foo(blah); }" I automatically think "blah contains an unmanaged resource that needs to be cleaned up aggressively as soon as foo is done (or throws) so that someone else can use that resource". I do not think "blah contains nothing interesting but there's some semantically important mutation that needs to happen, and we're going to represent that semantically important operation by the character '}' " -- some mutation like some stack has to be popped or some flag has to be reset or whatever.
I say that if you have a semantically important operation that has to be done when something completes, we have a word for that and the word is "finally". If the operation is important, then it should be represented as a statement that you can see right there and put a breakpoint on, not an invisible side effect of a right-curly-brace.
So let's think about your particular operation. You want to represent:
That code is perfectly clear. All the side effects are right there, visible to the maintenance programmer who wants to understand what your code is doing.
Now you have a decision point. What if blah throws? If blah throws then something unexpected happened. You now have no idea whatsoever what state "form" is in; it might have been code in "form" that threw. It could have been halfway through some mutation and is now in some completely crazy state.
Given that situation, what do you want to do?
1) Punt the problem to someone else. Do nothing. Hope that someone else up the call stack knows how to deal with this situation. Reason that the form is already in a bad state; the fact that its cursor is not in the right place is the least of its worries. Don't poke at something that is already so fragile, it's reported an exception once.
2) Reset the cursor in a finally block and then report the problem to someone else. Hope -- with no evidence whatsoever that your hope will be realized -- that resetting the cursor on a form that you know is in a fragile state will not itself throw an exception. Because, what happens in that case? The original exception, which perhaps someone knew how to handle, is lost. You've destroyed information about the original cause of the problem, information which might have been useful. And you've replaced it with some other information about a cursor-moving failure which is of use to no one.
3) Write complex logic that handles the problems of (2) -- catch the exception, and then attempt to reset the cursor position in a separate try-catch-finally block which suppresses the new exception and re-throws the original exception. This can be tricky to get right, but you can do it.
Frankly, my belief is that the correct answer is almost always (1). If something horrid went wrong, then you cannot safely reason about what further mutations to the fragile state are legal. If you don't know how to handle the exception then GIVE UP.
(2) is the solution that RAII with a using block gives you. Again, the reason we have using blocks in the first place is to aggressively clean up vital resources when they can no longer be used. Whether an exception happens or not, those resources need to be cleaned up rapidly, which is why "using" blocks have "finally" semantics. But "finally" semantics are not necessarily appropriate for operations which are not resource-cleanup operations; as we've seen, program-semantics-laden finally blocks implicitly make the assumption that it is always safe to do the cleanup; but the fact that we're in an exception handling situation indicates that it might not be safe.
(3) sounds like a lot of work.
So in short, I say stop trying to be so smart. You want to mutate the cursor, do some work, and unmutate the cursor. So write three lines of code that do that, done. No fancy-pants RAII is necessary; it just adds unnecessary indirection, it makes it harder to read the program, and it makes it potentially more brittle in exceptional situations, not less brittle.
编辑:显然,埃里克和我对这种用法存在分歧。 :o
你可以使用这样的东西:
Edit: Clearly Eric and I are at a disagreement on this usage. :o
You can use something like this:
如果您想在 C# 中执行类似 RAII 的操作,请使用 IDisposable 模式
Use the IDisposable pattern if you want to do RAII-like things in C#