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

时间:2010-02-24 01:49:02

标签: c# idisposable temporary-directory

我想提供一个类来管理创建和随后删除临时目录。理想情况下,我希望它可以在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方法。当我们尝试删除目录时,我们可能会失败;例如,因为我们仍然在其中打开一个文件。但是,如果我们允许传播异常,它可能会掩盖使用块内发生的异常。特别是,如果在使用块内发生异常,则可能是导致我们无法删除目录的异常,但如果我们屏蔽它,我们就丢失了解决问题最有用的信息

我们似乎有四种选择:

  1. 尝试删除目录时捕获并吞下任何异常。我们可能没有意识到我们没有清理我们的临时目录。
  2. 以某种方式检测Dispose是否在抛出异常时作为堆栈展开的一部分运行,如果是,则抑制IOException或抛出合并IOException的异常以及抛出的任何其他异常。可能甚至不可能。 (我之所以考虑到这一点,部分是因为它可以使用Python的context managers,它在许多方面类似于与C#的using语句一起使用的.NET的IDisposable。)
  3. 永远不要禁止IOException删除目录。如果在使用块中抛出异常,我们将隐藏它,尽管它有很大的可能性比IOException具有更多的诊断价值。
  4. 放弃删除Dispose方法中的目录。该类用户必须对请求删除目录负责。这似乎并不令人满意,因为创建课程的动机很大一部分是为了减轻管理这种资源的负担。也许还有另一种方法可以提供这种功能而不会让它变得非常容易搞砸?
  5. 其中一个选项显然最好吗?有没有更好的方法在用户友好的API中提供此功能?

7 个答案:

答案 0 :(得分:7)

不要将其视为实现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)。如果您尝试Dispose出现故障的通道,则会抛出异常并且屏蔽堆栈中已有的任何异常。如果我没弄错的话,TransactionScope也可以这样做。

当然,WCF中的这种行为一直是混乱的源头;大多数人实际上认为如果不打破它会很烦人。谷歌“WCF处理面具”,你会明白我的意思。因此,也许我们不应该总是尝试像微软那样做事情。

我个人认为Dispose永远不应该掩盖堆栈上已有的异常。 using语句实际上是一个finally块,而大部分时间都是(总是存在边缘情况),您不希望抛出(而不是捕获)异常finally阻止。原因只是调试;它可能极其很难找到问题的根源 - 特别是在生产中你无法跨过源头的问题 - 当你甚至没有能力找到确切的位置时该应用程序失败了。我之前一直处于这个位置,我可以自信地说它会让你完全彻底疯狂。

我的建议是要在Dispose中吃掉例外(当然是记录它),或者实际检查你是否already in a stack-unwinding scenario due to an exception,如果你知道你会掩饰他们。后者的优点是除非你真的需要,否则不要吃异常;缺点是你已经在程序中引入了一些非确定性行为。又一个权衡。

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

答案 1 :(得分:2)

最终,我建议最好按照FileStream作为指导,这相当于选项3和4:关闭文件或删除Dispose方法中的目录,并允许任何发生的异常冒泡的一部分(有效地吞下using块内发生的任何异常)但允许手动关闭资源而不需要使用块,如果组件的用户选择的话。

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

答案 2 :(得分:0)

这里要问的问题是调用者是否可以有效地处理异常。如果没有什么可以合理地使用(手动删除目录中使用的文件?),最好记录错误并忘掉它。

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

public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

然后,您可以将决定推送给类的用户,以确定适当的行为。

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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

<强>更新 我会根据以下事实采取此选项,因为操作可能会因为外部干扰而失败,例如来自另一个进程的锁定,并且因为目录放在系统临时目录中,所以我不会看到抛出异常的优点。 / p>

对该例外的有效回应是什么?试图再次删除该目录是不合理的,如果原因是来自另一个进程的锁定,那么它就不会直接由您控制。

答案 5 :(得分:0)

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

然而,如果发生其他事情,例如如果变量为null,则可能确实存在错误,然后该异常很有价值。

如果您预计锁定的文件,并且您或您的呼叫者可能会对您有所了解,那么您需要在您的课程中包含该响应。如果你能回应,那就在一次性电话中做。如果您的来电者可能会做出回应,请向您的来电者提供一种方法,例如一个TempfilesLocked事件。

答案 6 :(得分:-1)

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

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