异常管理的艺术与实践

在生产环境中,是否曾花费大量时间试图找出问题的根源?在这一过程中,是否遇到过以下情况之一:

异常管理是软件开发中至关重要且常被低估的主题之一。开发者在开发业务需求时可能会忽视日志记录和异常管理,因为它们在正常情况下似乎毫无作用。然而,当生产环境中出现异常时,由于缺乏日志记录和异常管理不善,解决问题可能需要数小时。

本文将讨论一些异常处理的实践,比如避免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); }

何时处理异常?

通常(但并非总是)最好在调用链的外部处理异常。但如果异常发生在一个不重要或可选的操作中,可能想要在日志文件中写入一个警告并忽略异常。然后,可能会在调用链的内部部分捕获异常。此外,可能想要捕获异常并写入有信息量的日志行,然后重新抛出异常以供调用链的外部部分处理。

使用代码

为了演示谈到的想法,创建了一个示例应用程序。这个示例应用程序包含非常简单的模型类,其中只包含将要使用的一些属性。

项目中的其他文件/类:

  • DBConnection:一个数据库模拟类,返回预定义的对象
  • TNullable:一个类,用于定义可空引用类型
  • EntityNotFoundException:一个自定义异常类
  • Example1、Example2、Example3:三个示例文件。详细信息如下
  • Program:应用程序的入口点。默认情况下,Main方法如下:
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引用。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485