65.9K
CodeProject 正在变化。 阅读更多。
Home

异常管理:假设和异常

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (8投票s)

2017年11月23日

CPOL

5分钟阅读

viewsIcon

14915

downloadIcon

210

异常处理实践,例如避免 null 值,编写可预测的函数签名,避免 null 引用异常等。

引言

您在生产环境中遇到过多少次“NullReferenceException”并花费数小时来找出问题根源?在此过程中,您是否遇到过以下陈述之一?

	return null;	

	return 0;

	return -1;

异常管理是至关重要的,也是软件开发中最重要的主题之一,但也是最被低估的主题之一。开发人员在开发业务需求时可能会忽略日志记录和异常管理。因为在正常情况下它们不会做任何事情。另一方面,当生产环境中发生异常时,由于缺乏日志记录和糟糕的异常管理,解决问题可能需要几个小时。

在本文中,我将介绍一些异常处理实践,例如避免 null 值,编写可预测的函数签名,避免 null 引用异常等。

背景

第一个问题是,为什么会发生异常?软件程序有几个函数和子函数。每个函数按顺序执行,并且可以具有输入和输出参数。通常,一个函数的输出成为另一个函数的输入(图 1-a)。在编写函数时,我们会处理这些输入和输出变量。我们有意识或无意识地对这些变量做出一些假设。如果我们的假设成立,我们的代码就能正常工作(图 1-a)。如果发生与我们的假设相矛盾的事情,那么我们的代码就会表现出预期之外的行为(图 1-b)。为了防止这些意外行为,我们通常会抛出异常。因此,良好的异常管理始于正确的假设。

图 1

如何做假设?

假设我们得到一个接口,其中包含以下两个函数。这两个函数中的一个返回 null(我们不知道是哪个),另一个在客户不存在时抛出异常。

Customer GetCustomerByEmail(String email);
Customer GetCustomerById(int customerId);

并且我们正在使用其中一个函数

Customer customer =  customerProvider.GetCustomerByEmail(email);
//Should I check null value?
CallCustomer(customer.PhoneNumber);

在使用 customer 变量之前,我们应该检查 null 值吗?即使我们知道这一点,我们也很容易忘记检查。我们也可能编写不必要的 null 检查块,这会使我们的代码混乱。但如果我们返回一个可空类型而不是 null 值,那么我们可以通知开发人员并强制他们像这样检查一个变量

TNullable<Customer> GetCustomerByEmail(String email);

TNullable<Customer> customer =  customerProvider.GetCustomerByEmail(email);
if (customer.HasValue()){
    CallCustomer(customer.Value.PhoneNumber)
}

请注意,Nullable<t> 类型不能与引用类型一起使用。所以我写了一个 TNullable<t> 类型,它可以与引用类型一起使用。幸运的是,可空引用类型已在 C# 8 中引入。如果您使用 C# 8,则无需创建新类型。

如果我们知道一个函数永远不会返回 null 值,那么我们就无需编写不必要的 null 检查块。

现在,我们可以再次关注这两个函数,并确定哪个应该返回 null,哪个应该抛出异常。

如果我们编写 GetCustomerById 方法作为我们将在内部使用的辅助方法,那么我们可以假设该方法始终接受一个现有客户数据的标识符。

//A SaleOrder has always an existing customer.
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;
}

现在,假设我们有一个网页,可用于通过电子邮件地址搜索 customer

TNullable<Customer> customer = db.GetCustomerByEmail(txtEmail.Text);

我们不能假设具有给定电子邮件地址的 customer 始终存在于数据库中。很有可能不存在具有给定电子邮件地址的客户。在这种情况下,与其返回 Customer 类型,不如返回一个可空的 Customer 类型(TNullable<Customer>)来通知开发人员该方法可能返回 null 值。

	public TNullable<Customer> GetCustomerByEmail(String email){
		Customer customer  = db.GetCustomerByEmail(email);
		return new TNullable<Customer>(customer);
	}

何时处理异常?

通常(但不总是)true,最好在调用链的外围部分处理异常。但如果异常发生在不重要或可选的操作中,您可以将警告写入日志文件并忽略异常。然后,您可以在调用链的内部部分捕获异常。此外,您可能希望捕获异常并向日志记录器写入信息性行,然后重新抛出异常以便由调用链的外围部分处理。

Using the Code

为了演示我所说的想法,我创建了一个示例应用程序。这个示例应用程序包含非常简单的模型类,这些类只有几个我们将要使用的属性。

项目中的其他文件/类

  • DBConnection:一个数据库模拟类,返回预定义的��象
  • TNullable:一个用于定义可空引用类型的类
  • EntityNotFoundException:一个自定义异常类
  • Example1Example2Example3:三个示例文件。详细信息如下
  • Program:应用程序的入口点。默认情况下,Main 方法如下所示
static void Main(string[] args)
{
	try
	{
		RunExample1(); 
		//RunExample2();
		//RunExample3();                 
	}
	catch (Exception ex)
	{
		LogEx(ex);
	}
	Console.ReadLine();
}

您可以运行其他示例。只需注释掉 RunExample1() 并取消注释其他行之一。

示例1

具有不良实践的初始代码。实际上,根本没有异常管理。运行后,您会收到“NullReferenceException”。您对导致该异常的原因一无所知。

示例 2

示例的更好实现。它抛出异常或为 null 值返回 TNullable 对象。现在我们知道异常发生在加载 AddressCity 引用时。但我们仍然不知道哪个订单的地址具有无效的 City 引用。

示例3

在第三个示例中,我添加了异常处理块,编写了有用的信息并重新抛出了异常。现在我们知道是哪个 orderaddresscity 数据导致了异常!

结论

我试图解释如何避免 null 引用,以及如何编写可预测的方法签名来帮助开发人员避免不必要的 null 检查块。我希望您喜欢这篇文章。请分享您的意见和建议。

© . All rights reserved.