您的同事似乎误解了“防御性编程”和/或例外情况。
防御性编程
防御性编程是关于防止某些类型的错误。
在这种情况下x.parent == null 是一个错误,因为您的方法需要使用x.parent.SomeField。如果parent 为空,那么SomeField 的值显然是无效的。使用无效值执行的任何计算或任务都会产生错误和不可预测的结果。
因此,您需要防范这种可能性。如果您发现x.parent == null,则抛出NullPointerException 是一种非常好的保护方法。该异常将阻止您从SomeField 获取无效值。它会阻止您使用无效值进行任何计算或执行任何任务。并且它将中止所有当前工作,直到错误得到适当解决。
注意异常是不是错误; parent 中的无效值是 实际 错误。异常实际上是一种保护机制。异常是一种防御性编程技术,它们是不可避免的。
由于 C# 已经抛出异常,因此您实际上不必执行任何操作。事实上,您的同事“以防御性编程的名义”所做的努力,实际上撤消语言提供的内置防御性编程。
例外情况
我注意到许多程序员对异常过度偏执。异常本身并不是错误,它只是报告错误。
您的同事说:“null 检查确保代码不会破坏应用程序”。这表明他认为异常会破坏应用程序。它们通常不会“破坏”整个应用程序。
异常可能如果糟糕的异常处理使应用程序处于不一致的状态,则会破坏应用程序。 (但如果错误被隐藏,这种情况更有可能发生。)如果异常“逃脱”线程,它们也可能破坏应用程序。 (转义主线程显然意味着你的程序已经相当不优雅地终止了。但即使转义子线程也已经够糟糕了,操作系统的最佳选择是 GPF 应用程序。)
但是,异常会中断(中止)当前操作。这是他们必须做的事情。因为如果你编写一个名为DoSomething 的方法,它调用DoStep1; DoStep1 中的 错误 意味着 DoSomething 无法正确地完成其工作。继续拨打DoStep2 毫无意义。
但是,如果在某个时候您可以完全解决特定错误,那么请务必:这样做。但请注意强调“完全解决”;这并不意味着只是隐藏错误。此外,仅仅记录错误通常不足以解决它。这意味着要达到这样的程度:如果另一个方法调用您的方法并正确使用它,“已解决的错误”不会对调用者正确完成其工作的能力产生负面影响。 (不管调用者是什么。)
也许完全解决错误的最佳示例是在应用程序的主处理循环中。它的工作是:等待队列中的一条消息,从队列中拉出下一条消息并调用适当的代码来处理该消息。如果在返回主消息循环之前引发了异常并且未解决,则需要解决它。否则异常会逃出主线程,应用程序会被终止。
许多语言在其标准框架中提供了默认的异常处理程序(具有程序员覆盖/拦截它的机制)。默认处理程序通常只会向用户显示一条错误消息,然后吞下异常。
为什么?因为如果您没有实现糟糕的异常处理,您的程序将处于一致状态。当前消息已中止,可以处理下一条消息,就好像没有任何问题一样。你当然可以覆盖这个处理程序:
- 写入日志文件。
- 发送调用堆栈信息以进行故障排除。
- 忽略某些类别的异常。 (例如
Abort 可能意味着您甚至不需要告诉用户,可能是因为您之前显示了一条消息。)
- 等
异常处理
如果您可以在不首先引发异常的情况下解决错误,那么这样做会更干净。但是,有时错误无法在首次出现的地方解决,或者无法提前检测到。在这些情况下,应该引发/抛出异常以报告错误,并通过实现异常处理程序(C# 中的 catch 块)来解决它。
注意:异常处理程序有两个不同的目的:首先,它们为您提供执行清理(或回滚代码)的地方,特别是因为存在错误/异常.其次,它们提供了解决错误和吞下异常的地方。 注意:在前一种情况下,重新引发/抛出异常非常重要,因为它尚未解决。
在关于抛出异常并处理它的评论中,您说:“我想这样做,但我被告知它会在任何地方创建异常处理代码。”
这是另一个误解。根据前面的旁注,您只需要在以下情况下进行异常处理:
- 您可以解决错误,在这种情况下您就完成了。
- 或者您需要在哪里实现回滚代码。
这种担忧可能是由于有缺陷的因果分析造成的。您不需要因为抛出异常而回滚代码。引发异常的原因还有很多。回滚代码是必需的,因为如果发生错误,该方法需要执行清理。换句话说,在任何情况下都需要异常处理代码。这表明对过度异常处理的最佳防御是设计,以减少对错误进行清理的需要。
所以不要“不抛出异常”以避免过多的异常处理。我同意过多的异常处理是不好的(参见上面的设计考虑)。但是在应该回滚的时候不回滚要糟糕得多,因为你甚至不知道有一个错误。