在这篇文章中,我使用了@Eric Lippert对异常的分类,你可以在这里找到: 令人烦恼的例外
在这种情况下,最重要的:
愚蠢的异常是你自己的错,你可以阻止它们,因此它们是你代码中的错误。你不应该抓住他们;这样做会在代码中隐藏错误。相反,您应该编写代码,以便首先不会发生异常,因此不需要捕获异常。
外源性异常似乎有点像令人烦恼的异常,只是它们不是不幸的设计选择的结果。相反,它们是凌乱的外部现实影响您美丽、清晰的程序逻辑的结果。始终处理指示意外外源条件的异常;一般来说,预测每一个可能的故障是不值得或不切实际的。只需尝试该操作并准备好处理异常。
就像每个开发人员可能经历过的那样,在大型企业软件中不可能避免 100% 的愚蠢异常。
在抛出愚蠢异常的不幸情况下,我想通知用户,以便他向我们报告错误(第三级支持)。此外,在这种情况下,我想记录日志级别为"错误"的消息。
对于外源异常,我想向用户显示带有一些提示的更具体的消息,因为他可能可以自己解决问题(也许在第一级或第二级支持的帮助下)
我目前实现这一目标的方法是在低级组件中显式捕获仅外源异常并将它们包装到自定义异常中。在顶层(在我的例子中是MVVM WPF应用程序的ViewModel)我然后 显式捕获自定义异常以显示警告。在第二个捕获块中,我捕获一般异常以显示错误。
这是区分企业应用程序中的笨拙异常和外源异常的常见和良好做法吗?有没有更好的方法?还是根本不需要?
阅读本文 dotnetpro - Implementierungsausnahmen 我也想知道,我是否应该将所有(也是愚蠢的)异常包装到自定义异常中,以便在记录它们时提供更多上下文信息?
关于包装所有异常,我发现以下帖子: stackoverflow - 我应该捕获并包装一般异常吗? 和堆栈溢出 - 我应该捕获所有可能的特定异常还是只是一般异常并将其包装在自定义异常中? 它非常有争议并且取决于用例,所以我不确定我的情况。
视图模型中的高级捕获处理程序的示例:
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);
}*/
}
}
尽管我们的代码中没有如此精确的分类,但我们的异常处理通常隐式地指示我们是否认为特定异常是可能的(外源的),或者我们是否只是在考虑可能的错误。
使用 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)
并且因此执行与任何其他异常不同的操作,那么我们很可能不会从中受益。
值得注意的是,几年前Microsoft建议我们的自定义异常继承自ApplicationException
而不是Exception
。这将区分自定义例外和系统例外。但很明显,这种区别并没有增加任何价值。我们不是说,"如果这是一个ApplicationException
做这个,否则就做那个。对于我们定义的其他自定义异常,通常也是如此。