3

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

在这种情况下最重要的:

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

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

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

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

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

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

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

在阅读了这篇文章dotnetpro - Implementierungsausnahmen 之后,我还想知道,是否应该将所有(也是愚蠢的)异常包装到自定义异常中,以便在记录它们时提供更多上下文信息?

关于包装我发现以下帖子的所有异常:stackoverflow - 我应该捕获并包装一般异常吗?stackoverflow - 我应该捕获所有可能的特定异常还是只捕获一般异常并将其包装在自定义异常中? 它似乎很有争议并且取决于用例,所以我不确定我的情况。

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

1 回答 1

2

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

使用 Eric 的示例,如果我们访问一个文件,将其放入 atry/catch并显式地 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这样,否则就那样。” 我们定义的其他自定义异常通常也是如此。

于 2019-07-09T17:42:02.337 回答