【问题标题】:How to distinguish unexpected (boneheaded) exceptions from expected (exogenous) ones?如何区分意外(愚蠢)异常和预期(外生)异常?
【发布时间】:2019-07-09 16:40:00
【问题描述】:

在这篇文章中,我使用了@Eric Lippert 对异常进行的分类,您可以在此处找到: Vexing exceptions

在这种情况下最重要的:

愚蠢的异常是您自己的错误,您可以阻止它们,因此它们是您代码中的错误。你不应该抓住它们;这样做会隐藏代码中的错误。相反,您应该编写代码,使异常不可能首先发生,因此不需要被捕获。

外部异常看起来有点像令人烦恼的异常,只是它们不是不幸的设计选择的结果。相反,它们是杂乱的外部现实影响你美丽、清晰的程序逻辑的结果。始终处理表示意外外部条件的异常;一般来说,预测每一个可能的失败是不值得或不切实际的。只需尝试操作并准备处理异常。

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

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

对于外生异常,我想向用户显示带有一些提示的更具体的消息,因为他可能自己解决问题(也许在一级或二级支持的帮助下)

我目前实现这一点的方法是,仅在低级组件中显式捕获外生异常,并将它们包装到自定义异常中。然后在顶层(在我的情况下是 MVVM WPF 应用程序的 ViewModel) 显式捕获自定义异常,以显示警告。在第二个 catch 块中,我捕获一般异常以显示错误。

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

读完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);
        }*/
    }
}

【问题讨论】:

    标签: c# exception architecture design-guidelines


    【解决方案1】:

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

    使用 Eric 的示例,如果我们访问一个文件,将其放入 try/catch,并显式捕获 FileNotFoundException,这应该表明我们意识到 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);
    

    如果在任何地方都没有一行代码写着 catch(DataStoreLoadException ex) 并且 结果与任何其他异常情况不同,那么我们很可能不会从中受益.

    值得注意的是,几年前微软建议我们的自定义异常继承自ApplicationException,而不是Exception。这将区分自定义和系统异常。但很明显,这种区别没有增加任何价值。我们不是说,“如果是ApplicationException,就这样做,否则就这样做。”我们定义的其他自定义异常通常也是如此。

    【讨论】:

    • 感谢您分享您的意见。结论:您不认为将愚蠢的异常与外生的异常区分开来是值得的吗? (能够在更高的日志级别上记录愚蠢的异常,例如“错误”,并向用户显示他应该联系支持人员的消息)否则您如何决定使用哪个日志级别?你向用户展示什么样的信息?如果你没有捕捉到特定的异常,你怎么能提供一些特定的、有用的信息呢? (如联系支持,以防愚蠢的异常)
    • 根据我的经验,不,它最终不会增加价值。某些事情没有按预期工作 - 无论出于何种原因,都无所谓 - 你最终会查看日志。你找到它并做出选择。可能是为了找出 SQL Server 没有响应的原因。可能是在代码中找到解决问题的方法。最大的区别,永远重要的区别,是介于有与无之间。如果您知道异常是什么以及抛出的位置,您就赢了。如果你能很快找到它,你赢的更快。
    • 但是“为什么SQL服务器没有响应”是客户可以自己解决的问题(如果数据库在客户端是本地的)。或者至少一级或二级支持可以帮助他。减少最终出现在我们开发人员桌面上的支持案例是否有帮助? (三级支持)
    • 这是一个更有用的区别。它是否需要用户采取行动,而不是“愚蠢的与外生的”?如果他们在一分钟内再试一次,它可能会改变吗? (一个愚蠢的错误可能会使 SQL Server 停止响应,所以这是一个有用的区别?)对用户来说重要的区别不是我们是否犯了一个愚蠢的错误,而是他们是否应该现在再试一次,稍后再打电话给某人,或者做其他事情。并确保我们提供给他们的信息是准确的。例如,如果他们搜索永远不会存在的东西,请不要告诉他们致电支持。
    猜你喜欢
    • 2019-08-16
    • 1970-01-01
    • 1970-01-01
    • 2014-03-11
    • 1970-01-01
    • 1970-01-01
    • 2015-06-25
    • 2017-07-17
    • 2019-09-16
    相关资源
    最近更新 更多