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