我应该如何处理 Dispose() 方法中的异常?

发布于 2024-08-22 22:02:01 字数 1574 浏览 3 评论 0原文

我想提供一个类来管理临时目录的创建和后续删除。理想情况下,我希望它可以在 using 块中使用,以确保无论我们如何离开该块,目录都会被再次删除:

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 块内发生异常,则可能导致我们无法删除目录,但如果我们屏蔽它,我们就丢失了解决问题的最有用信息。

看来我们有四个选择:

  1. 尝试删除目录时捕获并吞下任何异常。我们可能没有意识到我们未能清理临时目录。
  2. 当抛出异常时,以某种方式检测 Dispose 是否作为堆栈展开的一部分运行,如果是,则抑制 IOException 或抛出一个合并 IOException 和抛出的其他异常的异常。甚至可能根本不可能。 (我想到这一点的部分原因是Python的上下文管理器是可能的< /a>,这在很多方面类似于与 C# 的 using 语句一起使用的 .NET 的 IDisposable。)
  3. 切勿抑制 IOException 导致删除目录失败。如果在 using 块内抛出异常,我们将隐藏它,尽管它很可能比 IOException 具有更多的诊断价值。
  4. 放弃在Dispose方法中删除目录。该类的用户必须对请求删除目录负责。这似乎并不令人满意,因为创建该类的很大一部分动机是为了减轻管理此资源的负担。也许有另一种方法可以提供此功能而又不会很容易搞砸?

这些选项之一显然是最好的吗?有没有更好的方法在用户友好的 API 中提供此功能?

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.
}

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.

It seems we have four options:

  1. 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.
  2. 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.)
  3. 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.
  4. 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?

Is one of these options clearly best? Is there a better way to provide this functionality in a user-friendly API?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

忘年祭陌 2024-08-29 22:02:01

不要将其视为实现 IDisposable 的特殊类,而应考虑正常程序流的情况:

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 块中,吞下任何异常以防止屏蔽原始异常?

我认为没有任何正确的答案 - 事实上,您只能有一个环境异常,因此您必须选择一个。然而,.NET Framework 确实开创了一些先例; WCF 服务代理 (ICommunicationObject) 就是一个示例。如果您尝试处置出现故障的通道,它会引发异常,并且屏蔽堆栈上已有的任何异常。如果我没记错的话,TransactionScope 也可以做到这一点。

当然,WCF 中的这种行为一直是造成无尽混乱的根源。事实上,大多数人认为它即使没有损坏也很烦人。谷歌“WCF dispose mask”,你就会明白我的意思。因此,也许我们不应该总是尝试像微软一样做事。

就我个人而言,我认为 Dispose 永远不应该屏蔽堆栈上已有的异常。 using 语句实际上是一个 finally 块,并且大多数时候(总是存在边缘情况),您不想抛出(并且也没有捕获)finally 块中的异常。原因很简单,就是调试;弄清问题的根源可能极其 - 特别是生产中的问题,您无法单步查看源代码 - 当您甚至没有能力找出确切的位置时该应用程序失败。我以前也经历过这种情况,我可以自信地说,这会让你彻底疯掉。

我的建议是要么处理 Dispose 中的异常(当然,记录它),或者实际检查一下您是否是 由于异常已经处于堆栈展开场景,并且仅当您知道要屏蔽随后的异常时才执行它们。后者的优点是除非确实有必要,否则你不会吃例外;缺点是您在程序中引入了一些不确定的行为。又一个权衡。

大多数人可能只会选择前一个选项,并简单地隐藏 finally (或 using)中发生的任何异常。

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);
}

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?

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.

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.

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.

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.

Most people will probably just go with the former option and simply hide any exception occurring in finally (or using).

请别遗忘我 2024-08-29 22:02:01

最终,我建议最好遵循 FileStream< /code>作为指导,相当于选项 3 和 4:在 Dispose 方法中关闭文件或删除目录,并允许该操作中出现的任何异常冒泡(有效地吞掉 using 块内发生的任何异常),但如果组件的用户选择,则允许手动关闭资源,而不需要 using 块。

与 MSDN 的 FileStream 文档不同,我建议您详细记录用户选择使用 using 语句时可能发生的后果。

Ultimately, I would suggest it's best to follow FileStream as a guideline, which equates to options 3 and 4: close files or delete directories in your Dispose method, and allow any exceptions that occur as part of that action to bubble up (effectively swallowing any exceptions that occurred inside the using block) but allow for manually closing the resource without the need for a using block should the user of the component so choose.

Unlike MSDN's documentation of FileStream, I suggest you heavily document the consequences that could occur should the user chose to go with a using statement.

微凉 2024-08-29 22:02:01

这里要问的一个问题是调用者是否可以有效地处理异常。如果用户无法合理执行任何操作(手动删除目录中正在使用的文件?),最好记录错误并忘记它。

为了涵盖这两种情况,为什么不使用两个构造函数(或构造函数的参数)?

public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

然后,您可以将决定推给类的用户,以了解适当的行为可能是什么。

一个常见的错误是目录无法删除,因为其中的文件仍在使用中:您可以存储未删除的临时目录列表,并允许在程序关闭期间第二次显式尝试删除的选项(例如 TemporaryDirectory.txt)。 TidyUp() 静态方法)。如果有问题的目录列表非空,则代码可能会强制垃圾收集来处理未关闭的流。

A question to ask here is whether the exception can be usefully handled by the caller. If there is nothing the use can reasonably do (manually delete the file in use in the directory?) it may be better to log the error and forget about it.

To cover both cases, why not have two constructors (or an argument to the constructor)?

public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

Then you can push the decision off to the user of the class as to what the appropriate behavior might be.

One common error will be a directory which cannot be deleted because a file inside it is still in use: you could store a list of undeleted temporary directories and allow the option of a second explicit attempt at deletion during program shutdown (eg. a TemporaryDirectory.TidyUp() static method). If the list of problematic directories is non-empty the code could force a garbage collection to handle unclosed streams.

别想她 2024-08-29 22:02:01

您不能依赖于可以以某种方式删除目录的假设。一些其他进程/用户/任何东西都可以同时在其中创建文件。防病毒软件可能正忙于检查其中的文件等。

您可以做的最好的事情是不仅拥有临时目录类,而且拥有临时文件类(将在临时目录的 using 块内创建)临时文件类应该(尝试)删除 Dispose 上的相应文件,这样您至少可以尝试进行清理。

You cannot rely on the assumption that you can somehow remove your directory. Some other process/the user/whatever can create a file in it in the meantime. An antivirus may be busy checking files in it, etc.

The best thing you can do is to have not only temporary directory class, but temporary file class (which is to be created inside using block of your temporary directory. The temporary file classes should (attempt to) delete the corresponding files on Dispose. This way you are guaranteed that at least an attempt of cleaning up has been done.

旧时浪漫 2024-08-29 22:02:01

我想说,从锁定文件的析构函数中抛出异常可以归结为使用异常来报告预期结果 - 你不应该这样做。

但是,如果发生其他情况,例如变量为空,则可能确实出现错误,此时异常就有价值了。

如果您预计文件会被锁定,并且您或潜在的调用者可以对它们执行某些操作,那么您需要在类中包含该响应。如果您可以响应,则只需在一次性呼叫中进行即可。如果您的呼叫者能够响应,请为您的呼叫者提供一种方法,例如 TempfilesLocked 事件。

I would say throwing an exception from the destructor for a locked file boils down to using using an exception to report an expected outcome - you shouldn't do it.

However if something else happens e.g. a variable is null, you may really have an error, and then the exception is valuable.

If you anticipate locked files, and there is something you, or potentially your caller, can do about them, then you need to include that response in your class. If you can respond, then just do it in the disposable call. If your caller may be able to respond, provide your caller with a way to to this e.g. a TempfilesLocked event.

我们只是彼此的过ke 2024-08-29 22:02:01

假设创建的目录位于系统临时文件夹中,例如 Path.GetTempPath 返回的文件夹,那么我将实现 Dispose 以便在删除时不会引发异常临时目录失败。

更新:
我会采取这个选项,因为操作可能会因为外部干扰而失败,例如来自另一个进程的锁定,而且由于该目录放置在系统临时目录中,我看不到抛出异常的优势。

对该异常的有效响应是什么?尝试再次删除该目录是不合理的,如果原因是来自另一个进程的锁定,那么它不直接在您的控制之下。

Assuming that the created directory is located in a system temporary folder like the one returned by Path.GetTempPath then I would implement the Dispose in order to not throw an exception if the deletion of the temporary directory fails.

Update:
I would take this option based on the fact that the operation could fail because off an external interference, like a lock from another process and also since the directory is placed in the system temporary directory I would not see an advantage of throwing an exception.

What would be a valid response to that exception? Trying to delete the directory again is not reasonable and if the reason is a lock from another process that it's something that it's not directly under your control.

白龙吟 2024-08-29 22:02:01

要在 using 语句中使用该类型,您需要实现 IDisposable 模式。

要创建目录本身,请使用Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)作为基础,并使用新的Guid作为名称。

To use the type in a using statement, you want to implement the IDisposable pattern.

For creating the directory itself, use Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) as a base and a new Guid as the name.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文