如何区分预期的(骨头的)异常与预期的(外来的)异常?

时间:2019-07-09 16:40:00

标签: c# exception architecture design-guidelines

在这篇文章中,我使用@Eric Lippert的异常分类,您可以在这里找到: Vexing exceptions

在这种情况下最重要的是:

  

带骨头的异常是您自己的糟糕错误,您可以防止它们,因此它们是代码中的错误。你不应该抓住他们;这样做是在代码中隐藏一个错误。相反,您应该编写代码,以使异常不可能首先发生,因此不需要捕获。

     

外部异常看起来有点像令人烦恼的异常,只是它们不是不幸的设计选择的结果。相反,它们是不整洁的外部现实影响您美丽,清晰的程序逻辑的结果。始终处理指示意外的外部条件的异常;通常,预测每种可能的故障都不值得或不切实际。只需尝试操作并准备处理异常即可。

就像每个开发人员都可能经历过的那样,在大型企业软件中不可能将骨头异常避免达到100%。

在不幸的情况下,抛出了骨头异常,我想通知用户,以便他将错误报告给我们(三级支持)。另外,在这种情况下,我想记录一条日志,日志级别为“错误”。

对于外部异常,我想向用户显示一些更具体的消息,并带有一些提示,因为他可以自己解决问题(可能需要一级或二级支持)

我目前实现此目标的方法是,仅在底层组件中明确捕获外部异常,然后将其包装到自定义异常中。然后在顶层(在我的情况下为MVVM WPF应用程序的ViewModel)中 明确捕获自定义异常,以显示警告。在第二个catch块中,我捕获了常规Exception来显示错误。

这是区分企业应用程序中的异常异常和外来异常的常见且​​良好的做法吗?有没有更好的方法?还是根本没有必要?

阅读本文dotnetpro - Implementierungs­ausnahmen之后,我还想知道,是否应该将所有(也是笨拙的)异常包装到自定义异常中,以便在记录它们时提供更多上下文信息?

关于包装所有异常,我发现以下帖子:stackoverflow - Should I catch and wrap general Exception?stackoverflow - Should I catch all possible specific exceptions or just general Exception and wrap it in custom one? 似乎很有争议,并且取决于用例,因此我不确定我的情况。

ViewModel中高级捕获处理程序的示例:

public class MainWindowViewModel
{
    private readonly ICustomerRepository _customerRepository;

    public MainWindowViewModel(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
        PromoteCustomerCommand = new DelegateCommand(PromoteCustomer);
    }

    public ICommand PromoteCustomerCommand { get; }

    private void PromoteCustomer()
    {
        try
        {
            Customer customer = _customerRepository.GetById(1);
            customer.Promote();
        }
        catch (DataStoreLoadException ex)
        {
            // A expected exogenous exception. Show a localized message with some hints and log as warning.
            Log(LogLevel.Warning, ex);
            ShowMessage("Unable to promote customer. It could not be loaded. Try to...", ex);
        }
        catch (Exception ex)
        {
            // A unexpected boneheaded exception. Show a localized message, so that the users contacts the support and log as error.
            Log(LogLevel.Error, ex);
            ShowMessage("Unable to promote customer because of an unknown error. Please contact support@example.com", ex);
        }
    }
}

低级异常包装的示例:

public class SqlCustomerRepository : ICustomerRepository
{
    public Customer GetById(long id)
    {
        try
        {
            return GetFromDatabase(id);
        }
        catch (SqlException ex)
        {
            // Wrap the exogenous SqlException in a custom exception. The caller of ICustomerRepository should not depend on any implementation details, like that the data is stored in a SQL database.
            throw new DataStoreLoadException($"Unable to get the customer with id {id} from SQL database.", ex);
        }

        // All other exceptions bubble up the stack without beeing wrapped. Is it a good idea, or should I do something like this to provide additional context? (Like the id in this case)
        /*catch (Exception ex)
        {
            throw new DataStoreException($"Unknown error while loading customer with id {id} from SQL database.", ex);
        }*/
    }
}

1 个答案:

答案 0 :(得分:1)

尽管我们的代码中没有非常精确的分类,但是我们的异常处理通常隐含地表明我们是否认为特定的异常是可能的(外在的)或只是考虑可能的错误。

以埃里克(Eric)的示例为例,如果我们访问文件,将其放在try/catch中,并明确捕获FileNotFoundException,则应该表示我们意识到了{{1} }是可能的结果,即使我们提前一毫秒检查它是否存在。

另一方面,如果我们的代码包含以下内容:

FileNotFoundException

...此建议,我们正在考虑骨头异常,这种异常可能在try { // do some stuff } catch(Exception ex) { // maybe log it } 中执行的代码中的任何地方发生。

(某种程度上)使他们与众不同的地方是,一个表明我们意识到这是可能的,并且对此予以了解释,而另一个则表示:“希望这里没有错。”

即使这种区别也不是很清楚。我们的文件访问代码可能在“模糊” try块中。我们知道,由于竞争条件,该文件很可能不存在。在那种情况下,我们只是让模糊的异常处理抓住它。那可能取决于需要发生什么。如果我们要删除文件,但事实证明该文件不存在,则无需执行任何操作。如果我们需要阅读它而现在不见了,那只是一个例外。如果结果与任何其他例外相同,那么捕获该特定例外可能对我们没有好处。

类似地,仅因为我们明确捕获了一个异常并不能保证它不是“白痴”。也许我做错了事,有时我的代码抛出了try/catch(Exception ex)。我不知道为什么要这么做,所以我添加了ObjectDisposedException。乍一看,似乎我知道我的代码中发生了什么,但我确实不知道。我应该找出问题并加以解决,而不是在不知道为什么会发生的情况下捕获异常。如果该应用程序有时无法运行并且我不知道为什么,那么我捕获异常的事实充其量是无用的或最有害的,因为它掩盖了正在发生的事情。


这并不是说我们应该在每个方法中添加catch(ObjectExposedException ex)语句以捕获“骨头”异常。这只是异常处理的一个示例,它说明了可能是或不是错误的异常的可能性。通常在每种方法中都没有用。我们可能会在边缘放置足够多的内容,以确保引发的任何异常至少都将被记录下来。


关于在新的异常中捕获和包装异常,通常取决于您打算如何使用正在创建的额外信息来进行处理。答案常常是“什么都没有”。

我们可以在应用程序的一层中抛出各种巧妙包装的自定义异常。然后另一层调用它并执行此操作:

try/catch

我们如何处理常见的自定义异常?我们只是记录了它,就像我们没有包裹它一样。当我们查看日志时,将忽略自定义异常,而仅查看原始异常的堆栈跟踪。它告诉我们异常来自什么程序集,类和方法。这可能就是我们所需要的。

如果包装的异常的目的是添加上下文,例如

try
{
    _otherLayer.DoSomeStuff();
}
catch(Exception ex)
{
    _logger.Log(ex);       
}

...可能很有用。如果我们不这样做,那么例外将是“序列不包含任何元素”。这还不清楚。

但是我们是否有可能从中获得完全相同的里程?

throw new DataStoreLoadException($"Unable to get the customer with id {id} from SQL database.", ex);

如果在任何地方都没有一行写着throw new Exception($"Unable to get the customer with id {id} from SQL database.", ex); 的代码,并且其结果与其他任何例外情况都不一样,那么我们可能没有任何好处从它。

值得注意的是,几年前,Microsoft建议我们的自定义异常继承自catch(DataStoreLoadException ex),而不是ApplicationException。这将区分自定义异常和系统异常。但是很明显,这种区别没有任何价值。我们并不是说,“如果是Exception,请这样做,否则请这样做。”对于我们定义的其他自定义异常,通常也是如此。