这段代码是防御性编程,还是不好的做法?

时间:2014-03-07 00:46:07

标签: c# defensive-programming

我与我的同事就这段代码进行了辩论:

var y = null;
if (x.parent != null)
    y = x.parent.somefield;

我的观点是,在代码所在的位置,x.parent不应该是空的。当它为空时,我们有一个严重的问题,我想知道它!因此,空检查不应该存在,并且让下游异常发生。

我的同事说这是防御性编程。并且空检查确保代码不会破坏应用程序。

我的问题是,这是防御性编程吗?还是一个不好的做法?

注意:重点不在于谁。我试图从这个例子中学习。

5 个答案:

答案 0 :(得分:15)

有趣的问题。从我的角度来看,是否包括检查是一个问题,即数据的验证程度,数据来自何处以及检查失败时会发生什么。

“x.parent不应该为零”是一个严肃的假设。你需要非常肯定它才能安全起来,当然还有经典的“它永远不会发生”.......直到它发生,这就是为什么我认为回顾这些可能性很有意思。

我认为有两件事需要考虑。

数据来自哪里?

  • 如果它来自同一类中的另一个方法,或来自某个相关类,因为你对它有或多或少的完全控制,放宽你的防御是合乎逻辑的,因为你可以合理地假设它不太可能要开始使用不良数据,或者如果发生这种情况,在调试/测试过程中尽早发现错误,甚至为它进行一些单元测试都是相当容易的。

  • 相反的情况是,如果用户输入的数据,或从文件或URL中读取的数据,通常是外部的任何内容。由于您无法控制程序正在处理的内容:无论如何,在以任何方式使用它之前,请尽可能彻底地验证它,因为您会遇到可能导致问题的伪造/丢失/不完整/不正确/恶意信息路径。

  • 中间案例可以是输入来自同一系统中的另一个层/层。决定是否进行完全验证或将其视为理所当然更加困难,因为它是另一个内部软件,但可能会在以后单独替换或修改。我倾向于在越过边界时再次验证。

如何处理验证?

  • 使用if(如您的示例中)简单地跳过某些作业或用法可能适用于某些情况。例如,如果用户输入了一些数据,而这只是显示工具提示或其他次要信息,则跳过可能是安全的。但是如果这段代码做了一些重要的事情,那反过来填充了一些强制条件或者执行了其他一些过程,那么这不是正确的方法,因为它会导致下一次代码运行出现问题。问题是,当你跳过某些代码时,必须是安全的,没有任何副作用或不必要的后果,否则你会隐藏一些错误,这在以后很难调试发展阶段。

  • 优先中止当前流程是早期验证的理想选择,当完全预期失败并且您确切知道如何响应它时。一个示例可能是缺少必填字段,进程被中断并向用户显示消息,询问缺少的信息。简单地忽略错误并不严重,但也不足以引发破坏正常程序流的异常。当然,根据您的体系结构,您仍然可以使用异常,但无论如何都要抓住它并优雅地恢复。

  • 当“不可能”真的发生时,抛出异常总是一种选择。在这种情况下,您无法为继续进行某些变更或仅取消当前流程提供合理的响应,这可能是由于某个地方的错误或错误输入,但重要的是您想要了解并拥有关于它的所有细节,所以最好的方法是让它尽可能大声爆炸,以便异常起泡并到达一个全局处理程序,中断所有内容,保存到日志文件/ DB /无论如何,发送一个崩溃向您报告并找到恢复执行的方法,如果这是可行或安全的。至少如果您的应用程序崩溃,请以最优雅的方式执行此操作,并留下痕迹以供进一步分析。

一如既往,这取决于具体情况。但是使用if来避免编写异常处理程序肯定是一种不好的做法。它必须始终存在,然后一些代码可能依赖它 - 无论是否适当 - 如果失败并不重要。

答案 1 :(得分:8)

看起来你的同事误解了"防御性编程"和/或例外。

防御性编程

防御性编程是为了防止某些类型的错误。

在这种情况下,x.parent == null是一个错误,因为您的方法需要使用x.parent.SomeField。如果parent为空,则SomeField的值显然无效。任何使用无效值执行或使用无效值执行的任务都会产生错误且不可预测的结果。

所以你需要防止这种可能性。保护的一个非常好的方法是,如果发现NullPointerException,则抛出x.parent == null。该例外将阻止您从SomeField获取无效值。它将阻止您进行任何计算或使用无效值执行任何任务。并且它将中止所有当前的工作,直到错误适当地解决

  

注意异常错误; parent中的无效值,即实际错误。例外是一种保护机制。例外是防御性编程技术,它们不是要避免的。

由于C#已经抛出异常,你实际上不必做任何。事实上,事实证明,你的同事在防御性编程中的努力"实际上是撤消 提供的内置防御性编程用语言。

例外

我注意到很多程序员对异常情况过于偏执。异常本身并不是一个错误,它只是报告错误。

你的同事说:"空检查确保代码不会破坏应用程序"。这表明他认为例外会破坏申请。他们通常不会打破"打破"整个申请。

  

异常可能会破坏应用程序,如果异常处理不当会导致应用程序处于不一致状态。 (但如果错误被隐藏,则更有可能。)如果异常“逃避”,它们也会破坏应用程序。一个线程。 (转义主线程显然意味着你的程序已经非常不合适地终止了。但即使转义子线程也足够糟糕,操作系统的最佳选择是GPF应用程序。)

然而异常会中断(中止)当前操作。这是他们必须做的事情。因为如果您编写一个名为DoSomething的方法,该方法会调用DoStep1; DoStep1中的错误表示DoSomething无法正常 。继续拨打DoStep2没有意义。

但是,如果在某些时候您可以 完全解决 特定错误,那么请务必:执行此操作。但请注意强调"完全解决&#34 ;;这并不仅仅意味着隐藏错误。此外,只记录错误通常不足以解决它。这意味着要达到以下目的:如果另一种方法调用您的方法并正确使用它,那么“已解决的错误”就会出现问题。不会对来电者正确的工作能力产生负面影响。 (无论调用者是什么。)

完全解决错误的最好例子可能是在应用程序的主处理循环中。它的工作是:等待队列中的消息,从队列中提取下一条消息并调用适当的代码来处理消息。如果在返回主消息循环之前引发了异常并且未解决,则需要解决该异常。否则异常将转义主线程,应用程序将被终止。

许多语言在其标准框架中提供了一个默认的异常处理程序(程序员可以覆盖/拦截它的机制)。默认处理程序通常只向用户显示错误消息,然后吞下异常。

为什么呢?因为如果您没有实现糟糕的异常处理,您的程序将处于一致状态。当前消息已中止,可以处理下一条消息,就好像没有任何错误一样。您当然可以将此处理程序覆盖为:

  • 写入日志文件。
  • 发送调用堆栈信息以进行故障排除。
  • 忽略某些类别的异常。 (例如Abort可能意味着您甚至不需要告诉用户,可能是因为您之前显示过消息。)

异常处理

如果您可以在没有首先引发异常的情况下解决错误,那么这样做会更清晰。但是,有时错误无法在首次出现时解决,或者无法提前检测到。在这些情况下,应该引发/抛出异常以报告错误,并通过实现异常处理程序(C#中的 catch 块)来解决它。

  

注意:异常处理程序服务器有两个不同的目的:首先它们为您提供了执行清理(或回滚代码)的位置,因为 错误/异常。其次,它们提供了一个解决错误和吞下异常的地方。 NB :在前一种情况下,重新引发/抛出异常非常重要,因为未经解决

在关于抛出异常并处理它的评论中,你说:" 我想这样做,但我被告知它在任何地方创建异常处理代码。"

这是另一种误解。根据前面的附注,您只需要处理异常处理:

  • 您可以解决错误,在这种情况下您已完成。
  • 或者您需要实现回滚代码的地方。

担忧可能是由于缺陷因果分析造成的。您不需要回滚代码只是因为您要抛出异常。还有许多其他原因可以抛出异常。回滚代码是必需的,因为如果发生错误,该方法需要执行清理。换句话说,在任何情况下都需要异常处理代码。这表明防止过度异常处理的最佳方法是 设计 ,以便减少对错误清理的需求。

所以不要" 不要抛出异常"避免过多的异常处理。我同意过多的异常处理是不好的(参见上面的设计考虑)。但是,当你应该不回滚时要糟糕得多,因为你甚至知道没有错误。

答案 2 :(得分:2)

我根本不称它为防御性编程 - 我称之为“la la la I not me you you programming”因为代码似乎实际上忽略了潜在的错误条件。

显然我们不知道你的代码中接下来会发生什么,但由于你没有包含else子句,我只能假设你的代码只是继续{ {1}} 实际上是x.parent

请记住“不应该为空”,“绝对,肯定保证永远不会为空”并不一定是同一件事;所以在这种情况下,当你试图取消引用null时,它可能会导致一个例外。

  

那么问题是 - 在您尝试解决的问题(“域”)的上下文中哪些更容易接受,这取决于您稍后要对y做什么。< / p>

  • 如果y在此代码之后成为y不是问题(假设您稍后会对null进行防御性检查)那么就可以了 - 尽管我个人我不喜欢那种风格 - 你最终会在防御性方面检查每一次去引用,因为你永远不能确定你是否是一个远离崩溃的空引用......

  • 如果y!=null在代码之后不能y,因为它会导致异常或稍后丢失数据,那么当您知道时不变量是不正确的。

答案 3 :(得分:1)

简而言之,我认为这不是一个防御性的编程。我同意那些认为此代码隐藏系统错误而不是暴露和修复的人。此代码违反了“快速失败”原则。

当且仅当x.parent是必需的非null属性(从上下文中看起来很明显)时才会出现这种情况。但是,如果x.parent是一个可选属性(即可以合理地具有空值)然后根据您表达的业务逻辑,此代码可以是正常的。

我一直在考虑使用空值(0,“”,空对象)而不是空值,这需要大量不相关的if语句。

答案 4 :(得分:-2)

经过几年思考这个问题,我使用了以下“防御性”编程风格 - 我的95%的方法返回字符串作为成功/失败的结果。

我返回string.Empty表示成功,如果失败则返回信息性文本字符串。在返回错误文本时,我会同时将其写入日志。

public string Divide(int numA, int numB, out double division)
{
    division = 0;
    if (numB == 0)
        return Log.Write(Level.Error, "Divide - numB-[{0}] not valid.", numB);

    division = numA / numB;
    return string.Empty;
}

然后我用它:

private string Process(int numA, int numB)
{
    double division = 0;
    string result = string.Empty;
    if (!string.IsNullOrEmpty(result = Divide(numA, numB, out divide))
        return result;

    return SendResult(division);
}

当您拥有日志监控系统时,它会让系统显示继续,但会通知管理员。