问题 在C#中处理文件时如何正确处理异常


我已经阅读了许多关于正确异常处理的博客/文章/书籍章节,但我仍然不清楚这个主题。我将尝试用以下示例来说明我的问题。

考虑具有以下要求的类方法:

  1. 接收文件路径列表作为参数
  2. 读取每个文件的文件内容,如果尝试这样做有任何问题,请跳过
  3. 返回表示文件内容的对象列表

所以规格很简单,这就是我开始编码的方式:

    public class FileContent
    {
        public string FilePath { get; set; }
        public byte[] Content { get; set; }

        public FileContent(string filePath, byte[] content)
        {
            this.FilePath = filePath;
            this.Content = content;
        }
    }

    static List<FileContent> GetFileContents(List<string> paths)
    {
        var resultList = new List<FileContent>();

        foreach (var path in paths)
        {
            // open file pointed by "path"
            // read file to FileContent object
            // add FileContent to resultList
            // close file
        }

        return resultList;
    }

现在请注意,规范中的2.表示该方法应该“跳过因某些原因无法读取内容的文件”。因此,可能有许多不同的原因发生(例如,文件不存在,文件访问由于缺乏安全权限而被拒绝,文件被锁定并被其他应用程序使用等等)但重点是我应该不在乎原因是什么,我只想尽可能读取文件的内容,否则就跳过文件。我不在乎错误是什么......

那么如何正确实现这个方法呢?

好的,正确的异常处理的第一个规则永远不会捕获一般的异常。所以这段代码不好:

    static List<FileContent> GetFileContents(List<string> paths)
    {
        var resultList = new List<FileContent>();

        foreach (var path in paths)
        {
            try
            {
                using (FileStream stream = File.Open(path, FileMode.Open))
                using (BinaryReader reader = new BinaryReader(stream))
                {
                    int fileLength = (int)stream.Length;
                    byte[] buffer = new byte[fileLength];
                    reader.Read(buffer, 0, fileLength);

                    resultList.Add(new FileContent(path, buffer));
                }
            }
            catch (Exception ex)
            {
                // this file can't be read, do nothing... just skip the file
            }
        }

        return resultList;
    }

适当的异常handlig的下一个规则是:只捕获你可以处理的特定异常。好吧,我不关心处理任何可以抛出的特定异常,我只想检查文件是否可以读取。我怎样才能以正确,最佳的方式做到这一点?


3652
2017-11-07 14:36


起源

没有您系统的要求,您的问题就无法解决。这些芒果是基于意见的,不会解决您的具体情况。 - Peter
真正适当的异常处理的第一条规则是永远不会捕获一般异常吗? - paparazzo
@peer:您所指的系统有哪些要求?你的意思是应用程序是WPF,控制台还是ASP.NET?请澄清。 - matori82
@Blam: msdn.microsoft.com/en-us/library/ms182137.aspx - matori82
如果你可以以编程方式检查异常,那么你应该这样做而不是try / catch块。所以你应该检查文件是否存在,检查你是否有权打开它,等等,而不是在这里使用try / catch块。 - Harrison


答案:


尽管捕获和吞下非特定例外通常不被视为良好做法,但风险往往被夸大。

毕竟,ASP.NET将捕获在处理请求期间抛出的非特定异常,并且在将其包装在HttpUnhandledException之后,将重定向到错误页面并继续愉快地继续它。

在您的情况下,如果您想要遵守指南,则需要提供可以抛出的所有异常的完整列表。我相信以下列表是完整的:

UnauthorizedAccessException IOException FileNotFoundException DirectoryNotFoundException PathTooLongException NotSupportedException (路径的格式不正确)。 SecurityException ArgumentException

您可能不希望捕获SecurityException或ArgumentException,而其他几个派生自 IOException,所以你可能想抓住 IOExceptionNotSupportedException 和 UnauthorizedAccessException


4
2017-11-07 15:41



你能不能写一个示例代码(基于我的上面的代码)来演示你的方法? - matori82
@ matori82 - 它与布兰德答案中的样本非常相似: stackoverflow.com/a/19839124/13087。基本上是要忽略的每个异常的一系列空catch块(在这种情况下:IOException,NotSupportedException和UnauthorizedAccessException)。 - Joe


您的要求很明确 - 跳过无法读取的文件。那么一般异常处理程序有什么问题呢?它允许您以简单,清晰,可读,可扩展和可维护的方式执行任务。

如果您希望在将来某个日期以不同的方式处理多个可能的异常,则可以在常规异常之上添加特定异常的catch。

所以你宁愿看下面的代码?请注意,如果您添加更多代码来处理文件读取,那么您 必须 在此列表中添加任何新的例外。这一切什么都不做?

try
{
    // find, open, read files
}
catch(FileNotFoundException) { }
catch(AccessViolation) { }
catch(...) { }
catch(...) { }
catch(...) { }
catch(...) { }
catch(...) { }
catch(...) { }

约定是指导,并且很好地坚持创建良好的代码 - 但不要过度复杂代码只是为了保持一些奇怪的正确礼仪感。

对我而言,正确的礼仪是不要在浴室里说话 - 永远。但当老板在那里向你打招呼的时候,你又打招呼了。因此,如果您不小心处理多个异常,则无需捕获每个异常。


编辑:所以我推荐以下内容

try
{
    // find, open, read files
}
catch { } // Ignore any and all exceptions

上面告诉我不关心抛出哪个异常。通过不指定异常,即使只是System.Exception,我也允许.NET默认为它。所以下面是完全相同的代码。

try
{
    // find, open, read files
}
catch(Exception) { } // Ignore any and all exceptions

或者,如果您要至少记录它:

try
{
    // find, open, read files
}
catch(Exception ex) { Logger.Log(ex); }  // Log any and all exceptions

4
2017-11-07 14:58



所以你建议我像第一篇文章中的示例代码中那样捕获常规异常? - matori82
@ matori82是的。如果您甚至没有记录此信息,那么您实际上并不需要 (Exception ex)。你可以写 catch{} 因为它默认为 catch(Exception){}。这将进入我的帖子。 - bland


我对这个问题的解决方案通常基于可能的例外情况。如果只有少数几个,我为每个指定catch块。如果有很多可能,我会抓住所有例外情况。强制开发人员总是捕获特定的异常会导致一些非常丑陋的代码。


2
2017-11-07 14:51





您正在一种方法中混合不同的操作,更改代码将使您更容易问题:

static List<FileContent> GetFileContents(List<string> paths)
{
    var resultList = new List<FileContent>();

    foreach (var path in paths)
    {
          if (CanReadFile(path){
                resultList.Add(new FileContent(path, buffer));
          }
    return resultList;
}

static bool CanReadFile(string Path){
     try{
         using (FileStream stream = File.Open(path, FileMode.Open))
            using (BinaryReader reader = new BinaryReader(stream))
            {
                int fileLength = (int)stream.Length;
                byte[] buffer = new byte[fileLength];
                reader.Read(buffer, 0, fileLength);
            }
     }catch(Exception){ //I do not care what when wrong, error when reading from file
         return false;
     }
     return true;
}

这样,CanReadFile会隐藏您的支票实施。您唯一需要考虑的是CanReadFile方法是否正确,或者是否需要错误处理。


2
2017-11-07 15:06



我知道这只是一个例子,但这只是检查文件可读性的一种非常昂贵的方法。这是用同样的方法测试它的一个很好的理由。 - j.i.h.
@ j.i.h。同意,我会尝试实际阅读该文件。文件可以被锁定,它被添加到列表和实际读取文件,甚至被用户删除。但是,您可以选择CanReadFile方法的不同实现,而无需更改添加等,CanReadFile可以进行单元测试和存根。 - Peter


在这种情况下你可以考虑的东西是 FileNotFoundException,你无法捕捉,因为它们太多,而且最普遍 Exception,还有一层 IOException

通常,您将尝试尽可能详细地捕获异常,但特别是如果您捕获异常而不实际使用它们来抛出错误,您可能会捕获一组异常。即使这样,你也会尝试尽可能具体


2
2017-11-07 14:59



感谢您尝试提供帮助并编辑我的评论,但您输入的内容根本不是我的意思。 - Voidpaw


在我看来,将例外分为三种类型。首先是您期望的异常并且知道如何恢复。其次是您知道在运行时可以避免的异常。第三个是你不期望在运行时发生的那些,但是无法避免或无法实际处理。

处理第一种类型,这些是对您的特定抽象级别有效的异常类,它们代表在该级别恢复的有效业务案例(在您的情况下,忽略。)

应该避免第二类例外 - 不要懒惰。应该允许第三类异常通过...您需要确保知道如何处理问题,否则您可能会使应用程序处于混乱或无效状态。

正如其他人所说,你可以通过向现有的try块添加更多的catch块来处理多个异常,它们按照它们出现的顺序进行评估,所以如果你必须处理从你也处理的其他异常派生的异常,那就使用更多具体的第一个。


1
2017-11-14 13:50





这重复了所说的内容,但希望在某种程度上让您更好地理解。

您在“跳过因某些原因无法读取内容的任何文件”中出现逻辑错误。

如果这个原因是您的代码中的错误,您不想跳过它。
您只想跳过与文件相关的错误的文件。
如果FileContent中的ctor抛出错误该怎么办?

例外是昂贵的。
我会测试FileExists(并仍然捕获异常)
我同意Joe列出的例外情况
来吧MSDN有明确的例子,说明如何捕获各种异常


1
2017-11-07 21:10