在生产环境中,是否曾花费大量时间试图找出问题的根源?在这一过程中,是否遇到过以下情况之一:
异常管理是软件开发中至关重要且常被低估的主题之一。开发者在开发业务需求时可能会忽视日志记录和异常管理,因为它们在正常情况下似乎毫无作用。然而,当生产环境中出现异常时,由于缺乏日志记录和异常管理不善,解决问题可能需要数小时。
本文将讨论一些异常处理的实践,比如避免null值、编写可预测的函数签名、避免空引用异常等。
首先,异常为什么会发生?软件程序包含多个函数和子函数。每个函数按顺序执行,并且可能有输入和输出参数。通常,一个函数的输出会成为另一个函数的输入(如图1-a)。在编写函数时,处理这些输入和输出变量,并且对这些变量做出一些假设,无论是有意识还是无意识。如果假设成立,代码就能正常工作(如图1-a)。如果发生与假设相矛盾的事情,代码就会表现出意想不到的行为(如图1-b)。为了防止这些意外行为,通常会抛出异常。因此,良好的异常管理始于正确的假设。
假设有一个接口,其中包含以下两个函数。这两个函数中的一个返回null(不知道是哪一个),另一个在找不到客户的情况下抛出异常。
Customer GetCustomerByEmail(String email);
Customer GetCustomerById(int customerId);
使用其中一个函数:
Customer customer = customerProvider.GetCustomerByEmail(email);
// 应该检查null值吗?
CallCustomer(customer.PhoneNumber);
应该在使用customer变量之前检查null值吗?即使知道,也可能很容易忘记检查。也可能写出不必要的null检查块,这会使代码变得混乱。但如果返回一个可空类型而不是null值,那么就可以通知开发者并迫使他们像这样检查一个变量:
TNullable GetCustomerByEmail(String email);
TNullable customer = customerProvider.GetCustomerByEmail(email);
if (customer.HasValue()) {
CallCustomer(customer.Value.PhoneNumber);
}
请注意,Nullable<t>类型不能与引用类型一起使用。所以写了一个TNullable<t>类型,它可以与引用类型一起使用。幸运的是,C# 8引入了可空引用类型,如果使用C# 8,就不需要创建一个新类型了。
如果知道一个函数永远不会返回null值,那么就不需要编写不必要的null检查块。
现在,可以再次关注这两个函数,并确定哪一个应该返回null,哪一个应该抛出异常。
// A SaleOrder总是有一个存在的客户。
Customer customer = customerProvider.GetCustomerById(order.CustomerId);
如果客户在数据库中不存在,这是一个意外行为。所以在这里抛出一个异常是好的。不要忘记在异常中记录有用的信息。建议使用自定义异常类。
public Customer GetCustomerById(int customerId) {
Customer customer = db.GetCustomerById(customerId);
if (customer == null)
throw new EntityNotFoundException("Customer not found. CustomerId:" + customerId);
return customer;
}
现在,假设有一个网页,可以用来通过电子邮件地址搜索客户。
TNullable customer = db.GetCustomerByEmail(txtEmail.Text);
不能假设数据库中总是存在一个具有给定电子邮件地址的客户。在这种情况下,与其返回一个Customer类型,不如返回一个可空的Customer类型(TNullable<Customer>)来通知开发者这个方法可能会返回一个null值。
public TNullable GetCustomerByEmail(String email) {
Customer customer = db.GetCustomerByEmail(email);
return new TNullable(customer);
}
通常(但并非总是)最好在调用链的外部处理异常。但如果异常发生在一个不重要或可选的操作中,可能想要在日志文件中写入一个警告并忽略异常。然后,可能会在调用链的内部部分捕获异常。此外,可能想要捕获异常并写入有信息量的日志行,然后重新抛出异常以供调用链的外部部分处理。
为了演示谈到的想法,创建了一个示例应用程序。这个示例应用程序包含非常简单的模型类,其中只包含将要使用的一些属性。
项目中的其他文件/类:
static void Main(string[] args) {
try {
RunExample1();
// RunExample2();
// RunExample3();
}
catch (Exception ex) {
LogEx(ex);
}
Console.ReadLine();
}
可以运行其他示例。只需注释掉RunExample1()并取消注释其他行之一。
Example1:初始代码,没有异常管理。当运行时,会得到一个"NullReferenceException"。不知道是什么导致了那个异常。
Example2:一个更好的实现示例。它抛出异常或返回TNullable对象以处理null值。现在知道异常发生在加载Address的City引用时。但仍然不知道哪个订单的地址有无效的City引用。