问题描述
我想提供一个类来管理临时目录的创建和后续删除.理想情况下,我希望它可以在using块中使用,以确保无论我们如何离开该块,都可以再次删除目录:
I'd like to provide a class to manage creation and subsequent deletion of a temporary directory. Ideally, I'd like it to be usable in a using block to ensure that the directory gets deleted again regardless of how we leave the block:
static void DoSomethingThatNeedsATemporaryDirectory()
{
using (var tempDir = new TemporaryDirectory())
{
// Use the directory here...
File.WriteAllText(Path.Combine(tempDir.Path, "example.txt"), "foo\nbar\nbaz\n");
// ...
if (SomeCondition)
{
return;
}
if (SomethingIsWrong)
{
throw new Exception("This is an example of something going wrong.");
}
}
// Regardless of whether we leave the using block via the return,
// by throwing and exception or just normally dropping out the end,
// the directory gets deleted by TemporaryDirectory.Dispose.
}
创建目录没有问题.问题是如何编写Dispose方法.当我们尝试删除目录时,我们可能会失败.例如,因为我们仍有一个打开的文件.但是,如果我们允许传播异常,则它可能掩盖了using块内发生的异常.特别是,如果using块内发生异常,则可能是导致导致我们无法删除该目录的原因,但是如果我们屏蔽了该目录,我们将失去修复该问题的最有用的信息.
Creating the directory is no problem. The problem is how to write the Dispose method. When we try to delete the directory, we might fail; for example because we still have a file open in it. However, if we allow the exception to propagate, it might mask an exception that occurred inside the using block. In particular, if an exception occurred inside the using block, it might be one that caused us to be unable to delete the directory, but if we mask it we have lost the most useful information for fixing the problem.
似乎我们有四个选择:
- 尝试删除目录时捕获并吞下任何异常.我们可能没有意识到我们无法清理临时目录.
- 以某种方式检测当引发异常时Dispose是否正在作为堆栈展开的一部分运行,如果是,则抑制IOException或引发将IOException与其他任何异常合并的异常.甚至不可能. (我之所以这样想,部分原因是Python的上下文管理器,这在很多方面都与C#的using语句一起使用的.NET的IDisposable相似.)
- 从不禁止IOException无法删除目录.如果在using块中引发了异常,我们将其隐藏,尽管很有可能它具有比我们的IOException更多的诊断价值.
- 放弃在Dispose方法中删除目录.该类的用户必须负责请求删除目录.这似乎并不令人满意,因为创建类的主要动机是减轻管理该资源的负担.也许有另一种方式提供此功能而又不容易搞砸?
- Catch and swallow any exception when trying to delete the directory. We might be left unaware that we're failing to clean up our temporary directory.
- Somehow detect if the Dispose is running as part of stack unwinding when an exception was thrown and if so either suppress the IOException or throw an exception that amalgamates the IOException and whatever other exception was thrown. Might not even be possible. (I thought of this in part because it would be possible with Python's context managers, which are in many ways similar to .NET's IDisposable used with C#'s using statement.)
- Never suppress the IOException from failing to delete the directory. If an exception was thrown inside the using block we will hide it, despite there being a good chance that it had more diagnostic value than our IOException.
- Give up on deleting the directory in the Dispose method. Users of the class must remain responsible for requesting deletion of the directory. This seems unsatisfactory as a large part of the motivation for creating the class was to lessen the burden of managing this resource. Maybe there's another way to provide this functionality without making it very easy to screw up?
这些选项之一显然是最好的吗?有没有更好的方法可以通过用户友好的API提供此功能?
Is one of these options clearly best? Is there a better way to provide this functionality in a user-friendly API?
推荐答案
不是将其视为实现IDisposable
的特殊类,而是考虑正常程序流是什么样的:
Instead of thinking of this as a special class implementing IDisposable
, think of what it would be like in terms of normal program flow:
Directory dir = Directory.CreateDirectory(path);
try
{
string fileName = Path.Combine(path, "data.txt");
File.WriteAllText(fileName, myData);
UploadFile(fileName);
File.Delete(fileName);
}
finally
{
Directory.Delete(dir);
}
这应该如何表现?这是完全相同的问题.您是将finally
块的内容保持原样,从而潜在地掩盖try
块中发生的异常,还是将Directory.Delete
包装在其自己的try-catch
块中,以便按顺序吞下任何异常防止掩盖原图?
How should this behave? It's the exact same question. Do you leave the content of the finally
block as-is, thereby potentially masking an exception that occurs in the try
block, or do you wrap the Directory.Delete
in its own try-catch
block, swallowing any exception in order to prevent masking the original?
我认为没有任何正确答案-事实是,您只能有一个环境例外,因此您必须选择一个.但是,.NET Framework确实树立了一些先例.一个示例是WCF服务代理(ICommunicationObject
).如果您尝试Dispose
有故障的通道,它将引发异常,并且将屏蔽堆栈中已经存在的所有异常.如果我没记错的话,TransactionScope
也可以做到这一点.
I don't think there's any right answer - the fact is, you can only have one ambient exception, so you have to pick one. However, the .NET Framework does set some precedents; one example is WCF service proxies (ICommunicationObject
). If you attempt to Dispose
a channel that is faulted, it throws an exception and will mask any exception that is already on the stack. If I'm not mistaken, TransactionScope
can do this too.
当然,在WCF中,这种行为是无穷无尽的混乱根源.实际上,大多数人认为这很烦人,即使没有损坏也是如此. Google"WCF处理面具",您将明白我的意思.因此,也许我们不应该总是尝试像Microsoft一样做事.
Of course, this very behaviour in WCF has been an endless source of confusion; most people actually consider it very annoying if not broken. Google "WCF dispose mask" and you'll see what I mean. So perhaps we shouldn't always try to do things the same way Microsoft does them.
就我个人而言,Dispose
应该永远不要掩盖堆栈中已经存在的异常. using
语句实际上是一个finally
块,并且在大多数情况下 (总是存在边沿情况),您不想在finally
块中引发(也不捕获)异常, 任何一个.原因仅仅是调试.很难甚至根本无法找出问题的根源,尤其是在生产中无法单步检查源头的问题时,甚至根本无法找出问题所在.应用失败.我以前曾担任过这个职位,我可以自信地说,它将完全使您发疯.
Personally, I'm of the mind that Dispose
should never mask an exception already on the stack. The using
statement is effectively a finally
block and most of the time (there are always edge cases), you would not want to throw (and not catch) exceptions in a finally
block, either. The reason is simply debugging; it can be extremely hard to get to the bottom of an issue - especially an issue in production where you can't step through the source - when you don't even have the ability to find out where exactly the app is failing. I've been in this position before and I can confidently say that it will drive you completely and utterly insane.
我的建议是要么吃掉Dispose
中的异常(当然,将其记录下来),要么实际检查您是否是由于异常而已在堆栈展开方案中,并且仅在知道要屏蔽它们的情况下才吃掉后续异常.后者的优点是除非确实需要,否则您不会吃异常.缺点是您在程序中引入了一些不确定性行为.另一个权衡.
My recommendation would be either to eat the exception in Dispose
(log it, of course), or actually check to see if you're already in a stack-unwinding scenario due to an exception, and only eat subsequent exceptions if you know that you'll be masking them. The advantage of the latter is that you don't eat exceptions unless you really have to; the disadvantage is that you've introduced some non-deterministic behaviour into your program. Yet another trade-off.
大多数人可能只会选择前一个选项,而只是隐藏finally
(或using
)中发生的任何异常.
Most people will probably just go with the former option and simply hide any exception occurring in finally
(or using
).
这篇关于我应该如何在Dispose()方法中处理异常?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!