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 或引发合并 IOException 和引发的任何其他异常的异常。甚至可能都不可能。(我之所以想到这一点,部分原因是它可以使用 Python 的上下文管理器,它在很多方面类似于与 C# 的 using 语句一起使用的 .NET 的 IDisposable。)
  3. 永远不要抑制 IOException 无法删除目录。如果在 using 块中抛出异常,我们将隐藏它,尽管它很有可能比我们的 IOException 具有更多的诊断价值。
  4. 放弃在 Dispose 方法中删除目录。该类的用户必须继续负责请求删除目录。这似乎不能令人满意,因为创建类的很大一部分动机是减轻管理此资源的负担。也许还有另一种方法可以提供此功能而不会很容易搞砸?

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

4

7 回答 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)。如果您尝试访问Dispose出现故障的通道,它会引发异常并将屏蔽堆栈中已经存在的任何异常。如果我没记错的话,TransactionScope也可以这样做。

当然,WCF 中的这种行为一直是混乱的源泉。大多数人实际上认为如果没有损坏它会很烦人。谷歌“WCF dispose mask”,你会明白我的意思。因此,也许我们不应该总是尝试以与微软相同的方式做事。

就个人而言,我认为Dispose永远不应该掩盖堆栈上已经存在的异常。该using语句实际上是一个finally块,并且大多数时候(总是存在边缘情况),您也不希望在finally块中抛出(而不是捕获)异常。原因只是调试;当您甚至无法找出应用程序出现故障的确切位置时,可能很难找到问题的根源——尤其是在生产中无法单步执行源代码的问题我以前曾担任过这个职位,我可以自信地说,这会让你彻底发疯。

我的建议是要么吃掉异常Dispose(当然是记录它),或者实际检查你是否已经由于异常而处于堆栈展开场景中,并且只有在你知道你的情况下才吃后续的异常会掩盖他们。后者的好处是你不吃异常,除非你真的不得不吃;缺点是您在程序中引入了一些非确定性行为。另一个权衡。

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

于 2010-02-24T03:37:27.443 回答
2

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

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

于 2010-02-24T02:11:51.040 回答
0

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

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

于 2010-02-24T08:36:25.030 回答
0

假设创建的目录位于系统临时文件夹中,就像那个时候返回的那样,如果临时目录的删除失败,Path.GetTempPath我会实现Dispose,以便不抛出异常。

更新: 我会选择这个选项,因为操作可能会因为外部干扰而失败,比如来自另一个进程的锁,而且由于目录被放置在系统临时目录中,我看不到抛出异常的好处.

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

于 2010-02-24T12:54:58.083 回答
0

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

但是,如果发生其他事情,例如变量为空,您可能确实有错误,然后异常是有价值的。

如果您预计文件会被锁定,并且您或可能是您的调用者可以对它们做些什么,那么您需要在您的课程中包含该响应。如果您可以响应,那么只需在一次性呼叫中进行即可。如果您的呼叫者能够响应,请为您的呼叫者提供解决此问题的方法,例如 TempfilesLocked 事件。

于 2010-02-24T13:21:48.950 回答
0

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

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

public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

然后,您可以将决定权交给班级的用户,以决定适当的行为可能是什么。

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

于 2010-02-24T02:37:46.857 回答
-1

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

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

于 2010-02-24T02:11:07.813 回答