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

15

我想提供一个管理创建和删除临时目录的类。理想情况下,我希望它可在 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和其他抛出的异常。可能不可行。(我提到这一点部分原因是因为Python的上下文管理器可以做到这一点,在许多方面类似于用C#的using语句使用的.NET中的IDisposable。)
  3. 永远不要压制无法删除目录的IOException。如果在using块内抛出异常,则尽管存在更好的诊断价值,我们也将隐藏它。
  4. 放弃在Dispose方法中删除目录。该类的用户必须继续负责请求删除目录。这似乎不太令人满意,因为创建该类的重要原因之一是减轻管理此资源的负担。也许有另一种方法可以提供此功能,而不会使其变得非常容易出错?

这四个选项中是否有一个明显最好的?是否有更好的方法在用户友好的API中提供此功能?

7个回答

8

不要将其视为实现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”,你就会知道我的意思。因此,也许我们不应总是尝试以Microsoft的方式来做事情。
个人而言,我认为Dispose不应该掩盖已经在堆栈上的异常。使用using语句实际上是一个finally块,并且大多数情况下(总会有边缘情况),您不想在finally块中抛出(并且不捕获)异常。原因很简单,就是调试;当您甚至无法找出应用程序确切失败的位置时,特别是在生产中出现问题时,这可能非常难以解决-尤其是无法通过源代码进行步骤时。我曾经处于这种境地,可以自信地说,这将使您彻底疯狂。

我的建议是在Dispose中吃掉异常(当然要记录日志),或者实际检查是否由于异常而已经处于堆栈展开的情况下, 并且仅在您知道将屏蔽它们时才吃掉后续异常。后一种方法的优点是,除非必须这样做,否则不会吃掉异常;缺点是引入了一些非确定性行为到您的程序中。又是一个权衡。

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


让我感到恼火的是,.net语言没有提供一种好的方式来知道finally块是否由于异常而运行(更不用说是什么异常了)。我认为,如果不是大多数,那也是相当多的catch块应该是fault块或者在finally中使用条件代码;只有在合理地期望解决异常或者将其主动包装时,才应该捕获异常,而不是简单地进行空白的throw。一个典型的例子:在IDisposable对象的构造函数中发生异常应该清理对象,但不应该被“捕获”。 - supercat

2

最终,我建议最好遵循FileStream作为指导方针,这相当于选项3和4:在您的Dispose方法中关闭文件或删除目录,并允许作为该操作一部分发生的任何异常上升(有效地吞咽了在using块内发生的任何异常),但应允许手动关闭资源,而无需使用块,如果组件的用户选择这样做。

与MSDN对FileStream的文档不同,我建议您严格记录用户选择使用using语句可能会导致的后果。


0
这里需要问一个问题,那就是调用者能否处理这个异常。如果用户无法合理地做任何事情(例如手动删除目录中正在使用的文件?),最好记录错误并忘记它。
为了同时涵盖这两种情况,为什么不设置两个构造函数(或者构造函数有一个参数)呢?
public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

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

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


啊,好老的“让用户决定”。也就是说,“我拒绝做出任何决定,因为有可能我会错。”这样做只是把问题上移,迫使你的类的消费者在不关心的时候考虑某些事情。 - Aaronaught
显然您是亨利·福特的“你可以选择任何颜色,只要是黑色”的设计模式的粉丝 :-) 有时顾客的选择也是好的!我怀疑第二个构造函数会有很少的接受者-但是没有了解应用程序,就像您所说的那样,没有正确的答案。 - MZB
实际上,你的代码是一种不好的实践方式,正如框架设计准则所描述的那样。以下是书中的一节:http://blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx 你可以在那里看到以下段落:“不要有公共成员可以根据某些选项抛出或不抛出异常。类型GetType(string name, bool throwOnError)”。 - nightcoder

0

您不能仅凭假设就删除目录,因为其他进程/用户/任何人可能在此期间在其中创建文件。防病毒软件可能正在忙于检查其中的文件等。

最好的做法是不仅拥有临时目录类,还应该具备临时文件类(它应该在临时目录的using块内被创建)。临时文件类应该(尝试)在Dispose中删除相应的文件。这样,您可以保证至少已经尝试了清理工作。


杀毒软件是一个好的点子 - 我之前没有真正考虑过这个。如果我们无法删除目录,那么这将使问题变得更加模糊,我们是否应该认为这是一个关键性失败。然而,谈论用户在我们的临时目录中创建文件 - 这是应用程序通常防范的事情吗?毕竟,用户可以通过在应用程序运行时搞乱应用程序的私有文件来破坏所有种类的东西 - 有多少应用程序实际上需要对此类事情进行强制性的鲁棒性要求? - Weeble

0
假设创建的目录位于系统临时文件夹中,例如由Path.GetTempPath返回的文件夹,则我会实现Dispose以避免在删除临时目录失败时抛出异常。
更新: 我会选择这个选项是因为操作可能会因外部干扰(如另一个进程的锁定)而失败,而且由于目录位于系统临时目录中,我不认为抛出异常有任何优势。
对于该异常,什么是有效的响应?再次尝试删除目录是不合理的,如果原因是另一个进程的锁定,则这是您无法直接控制的事情。

我开始认为这可能是最好的选择,但您有任何特别的理由吗?是因为在这种情况下它特别容易失败吗? - Weeble

0

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

然而,如果发生其他情况,例如变量为空,那么您可能真的有一个错误,这时异常是有价值的。

如果您预计会出现锁定文件,并且有一些您或潜在的调用者可以对其进行处理的事情,那么您需要在类中包含该响应。如果您可以响应,则只需在可处置的调用中执行即可。如果您的调用者可能能够响应,请为其提供一种方法,例如TempfilesLocked事件。


-1

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

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


1
我认为@Weeble理解了处理模式,但是ta关注的是在Dispose方法中可能抛出异常的后果。 - Randolpho

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接