【问题标题】:How should I handle exceptions in my Dispose() method?我应该如何处理我的 Dispose() 方法中的异常?
【发布时间】:2010-02-24 01:49:01
【问题描述】:

我想提供一个类来管理临时目录的创建和后续删除。理想情况下,我希望它可以在 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 的 context managers,它在很多方面类似于与 C# 的 using 语句一起使用的 .NET 的 IDisposable。)
  3. 永远不要抑制 IOException 无法删除目录。如果在 using 块中抛出异常,我们将隐藏它,尽管它很有可能比我们的 IOException 具有更多的诊断价值。
  4. 放弃在 Dispose 方法中删除目录。该类的用户必须继续负责请求删除目录。这似乎不能令人满意,因为创建类的很大一部分动机是减轻管理此资源的负担。也许还有另一种方法可以提供此功能而不会很容易搞砸?

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

【问题讨论】:

    标签: c# idisposable temporary-directory


    【解决方案1】:

    与其将其视为实现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 中的异常(当然要记录它),或者实际检查你是否是already in a stack-unwinding scenario due to an exception,并且只有在你知道你会是时才吃后续的异常掩盖他们。后者的好处是你不吃异常,除非你真的不得不吃;缺点是您在程序中引入了一些非确定性行为。另一个权衡。

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

    【讨论】:

    • 令我恼火的是,.net 语言没有提供一个很好的方法来了解 finally 块是否由于异常而运行(更不用说它是什么了)。我假设大部分catch 块应该是fault 块或finally 中的条件代码;代码应该只捕获一个异常,如果它有一个合理的期望解决它,或者会主动包装它,而不是简单地做一个空白throw。主要示例:IDisposable 对象的构造函数中的异常应清理对象,但不应被“捕获”。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

      public TemporaryDirectory()
      : this( false )
      {
      }
      
      public TemporaryDirectory( bool throwExceptionOnError )
      {
      }
      

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

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

      【讨论】:

      • 啊,老“让用户决定”。 AKA “我拒绝做出任何决定,因为我有可能是错的。”所有这一切都是将问题转移到上游,迫使您班级的消费者考虑他们当时真正不关心的事情。
      • 显然你是亨利福特的粉丝“你可以有任何你想要的颜色,只要它是黑色的”设计模式:-) 有时客户的选择也很好!我怀疑第二个构造函数会有很多接受者——但正如你所说,在不知道应用程序的情况下,没有正确的答案。
      • 实际上,您的代码是一种不好的做法,如框架设计指南中所述。这是书中的一段:blogs.msdn.com/b/kcwalina/archive/2005/03/16/396787.aspx 您可以在那里看到以下段落:“没有可以根据某些选项抛出或不抛出的公共成员。键入 GetType(string name, bool throwOnError)”。
      【解决方案4】:

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

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

      【讨论】:

      • 防病毒软件是个好点 - 我没有真正考虑过这一点。如果我们不能删除目录,我们是否应该将其视为严重失败,这使得它成为一个更加模糊的问题。然而,谈到 user 在我们的临时目录中创建文件 - 这是应用程序通常要防范的事情吗?毕竟,用户可以通过在应用程序运行时弄乱应用程序的私有文件来破坏各种事情——实际上需要多少应用程序才能对这类事情保持健壮?
      【解决方案5】:

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

      但是,如果发生其他事情,例如一个变量为null,你可能真的有错误,然后异常是有价值的。

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

      【讨论】:

        【解决方案6】:

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

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

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

        【讨论】:

        • 我开始认为这可能是最好的,但你有什么特别的理由吗?是不是因为在这种情况下特别容易失败?
        【解决方案7】:

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

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

        【讨论】:

        • 我认为@Weeble 了解处置模式,(s?)他担心Dispose 方法中可能引发的异常的后果。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-04-01
        • 1970-01-01
        • 2014-02-21
        • 2011-02-24
        • 1970-01-01
        • 2012-04-16
        相关资源
        最近更新 更多