【问题标题】:Programming style: should you return early if a guard condition is not satisfied?编程风格:如果不满足守卫条件,是否应该提前返回?
【发布时间】:2010-05-28 11:31:55
【问题描述】:

我有时想知道的一件事是下面显示的两种风格中哪一种更好(如果有的话)?如果没有满足保护条件,是立即返回更好,还是只在满足保护条件 时才做其他事情?

为了论证,请假设保护条件是一个简单的返回布尔值的测试,例如检查一个元素是否在集合中,而不是通过抛出异常可能影响控制流的东西.还假设方法/函数足够短,不需要编辑器滚动。

// Style 1
public SomeType aMethod() {
  SomeType result = null;

  if (!guardCondition()) {
    return result;
  }

  doStuffToResult(result);
  doMoreStuffToResult(result);

  return result;
}

// Style 2
public SomeType aMethod() {
  SomeType result = null;

  if (guardCondition()) {
    doStuffToResult(result);
    doMoreStuffToResult(result);
  }

  return result;
}

【问题讨论】:

  • 我的经验法则:如果保护条件失败,则尽早返回,除非严重影响可读性,否则在保护条件之后单点返回。

标签: language-agnostic control-flow program-flow


【解决方案1】:

我更喜欢第一种样式,只是在不需要时我不会创建变量。我会这样做:

// Style 3
public SomeType aMethod() {

  if (!guardCondition()) {
    return null;
  }

  SomeType result = new SomeType();
  doStuffToResult(result);
  doMoreStuffToResult(result);

  return result;
}

【讨论】:

  • 鉴于它可以设置为nullSomeType 必须是某种指针,所以您的意思可能是SomeType result = new SomeTypeInternal();,其中typedef SomeTypeInternal* SomeType;。也就是说,延迟施工直到您真正需要某些东西是一个非常非常好的主意。太多从 C 或 Pascal 开始的人忘记了您可以在 C++ 中的任何时候声明变量。
  • 是的,我打算用伪代码给出示例,但它们最终以 Java 形式出现!
  • 啊。我不使用Java,所以我认为它是C++。对不起。问题上的“Java”标签会有所帮助。
  • @Mike:实际上,正确的标签应该是language-agnostic,问题不是关于指针语义或类似的语言细节,而是关于是否从一个方法返回/function 早晚。
  • @Tim:语法可能是 Java,但问题不在于 Java。它通常是关于命令式编程的。 (它几乎立即适用于 C#、C 和 C++,并且对 Python、Perl、Ruby、Tcl 进行了更多的语法更改......)它非常不可知论。
【解决方案2】:

在 80 年代后期接受 Jackson 结构化编程培训后,我根深蒂固的理念始终是“一个函数应该有一个入口点和一个出口点”;这意味着我按照样式 2 编写代码。

在过去的几年里,我开始意识到以这种风格编写的代码通常过于复杂且难以阅读/维护,因此我已切换到风格 1。

谁说老狗学不会新把戏? ;)

【讨论】:

  • 哇,这又带回了噩梦。谈谈排除例外的概念。
【解决方案3】:

样式 1 是 Linux 内核间接推荐的。

来自https://www.kernel.org/doc/Documentation/process/coding-style.rst,第 1 章:

现在,有些人会声称拥有 8 个字符的缩进 代码向右移动太远,难以阅读 80 个字符的终端屏幕。答案是如果你需要 超过 3 级的缩进,无论如何你都搞砸了,应该修复 你的程序。

样式 2 增加了缩进级别,因此不鼓励这样做。

就我个人而言,我也喜欢风格 1。样式 2 使得在具有多个保护测试的函数中匹配右大括号变得更加困难。

【讨论】:

    【解决方案4】:

    我不知道 guard 是否是正确的词。通常,不满意的守卫会导致异常或断言。
    但除此之外,我会选择样式 1,因为我认为它可以让代码更简洁。您有一个只有一个条件的简单示例。但是,许多条件和样式 2 会发生什么?它会导致大量嵌套的 ifs 或巨大的 if 条件(||&&)。我认为最好尽快从方法返回。
    但这当然是非常主观的^^

    【讨论】:

      【解决方案5】:

      Martin Fowler 将此重构称为: "Replace Nested Conditional with Guard Clauses"

      If/else 语句也会带来圈复杂度。因此更难测试用例。为了测试所有 if/else 块,您可能需要输入很多选项。

      如果有保护子句,你可以先测试一下,把if/else子句里面的真正逻辑处理得更清楚。

      【讨论】:

        【解决方案6】:

        如果您使用 .net-Reflector 深入研究 .net-Framework,您会看到 .net 程序员使用样式 1(或者可能是 unbeli 已经提到的样式 3)。 上面的答案已经提到了原因。或许还有一个原因是为了让代码更易读、更简洁、更清晰。 这种风格最常使用的是在检查输入参数时,如果你编写一种框架/库/dll,你总是必须这样做。 首先检查所有输入参数而不是使用它们。

        【讨论】:

          【解决方案7】:

          这有时取决于您使用的语言和“资源”类型(例如打开的文件句柄)。

          在 C 语言中,样式 2 绝对更安全、更方便,因为函数必须关闭和/或释放它在执行期间获得的任何资源。这包括分配的内存块、文件句柄、操作系统资源的句柄(如线程或绘图上下文)、互斥锁以及任何数量的其他事物。将return 延迟到最后或以其他方式限制函数的退出次数允许程序员更轻松地确保她/他正确清理,有助于防止内存泄漏、处理泄漏、死锁和其他问题。

          在使用RAII 风格编程的C++ 中,两种风格同样安全,因此您可以选择更方便的一种。我个人使用风格 1 和 RAII 风格的 C++。没有 RAII 的 C++ 就像 C 一样,所以同样,在这种情况下,样式 2 可能更好。

          在像 Java 这样带有垃圾收集的语言中,运行时有助于消除两种风格之间的差异,因为它会自行清理。但是,如果您没有明确地“关闭”某些类型的对象,这些语言也可能存在微妙的问题。例如,如果您构造一个新的java.io.FileOutputStream 并且在返回之前没有close 它,那么关联的操作系统句柄将保持打开状态,直到运行时垃圾收集已超出范围的FileOutputStream 实例。这可能意味着在收集FileOutputStream 实例之前,需要打开文件进行写入的另一个进程或线程可能无法打开。

          【讨论】:

          • 在 Java 中你应该使用 finally 来处理诸如关闭文件流之类的事情。
          【解决方案8】:

          虽然它违背了我所学过的最佳实践,但我发现当我遇到这种情况时减少 if 语句的嵌套要好得多。我认为它更容易阅读,虽然它存在多个地方,但仍然很容易调试。

          【讨论】:

          • 谁说它“违背了最佳实践”?
          • 老实说,我只从老师那里听说过。我想这并不是该领域的最佳实践。
          【解决方案9】:

          我会说 Style1 变得更常用,因为如果您将它与小方法结合使用,这是最佳实践。

          当你有大方法时,Style2 看起来是一个更好的解决方案。当你拥有它们时......无论你如何退出,你都有一些想要执行的通用代码。但正确的解决方案不是强制单个退出点,而是使方法更小。

          例如,如果你想从一个大方法中提取一个代码序列,而这个方法有两个退出点,你开始遇到问题,很难自动完成。当我有一个用 style1 编写的大方法时,我通常将其转换为 style2,然后我提取方法,然后在每个方法中我应该有 Style1 代码。

          所以 Style1 是最好的,但与小方法兼容。 Style2 不太好,但如果你有不想要的大方法,有时间拆分。

          【讨论】:

          • 我真的不明白,为什么这不是公认的答案。它是迄今为止唯一解决长短函数问题的方法。这里的其他所有内容更多地是个人喜好的答案,或者两种风格都没有任何论据。
          【解决方案10】:

          我自己更喜欢使用方法#1,它在逻辑上更容易阅读,并且在逻辑上也更类似于我们正在尝试做的事情。 (如果发生不好的事情,立即退出函数,不要通过 go,不要收取 200 美元)

          此外,大多数情况下,您可能希望返回一个逻辑上不可能的结果(即 -1),以向调用函数的用户表明该函数未能正确执行并采取适当的措施。这也更适合方法 #1。

          【讨论】:

            【解决方案11】:

            我会说“这取决于...”

            在我必须在离开函数/方法之前执行超过 2 或 3 行的清理序列的情况下,我更喜欢样式 2,因为清理序列只需编写和修改一次。这意味着可维护性更容易。

            在所有其他情况下,我更喜欢样式 1。

            【讨论】:

              【解决方案12】:

              1 号通常是简单、懒惰和草率的方式。数字 2 清晰地表达了逻辑。其他人指出的是,是的,它会变得很麻烦。这种趋势虽然有一个重要的好处。样式 #1 可以隐藏您的功能可能做得太多。它并没有很好地直观地展示正在发生的事情的复杂性。 IE。它可以防止代码对您说“嘿,这对于这个功能来说有点太复杂了”。它还使其他不了解您的代码的开发人员更容易错过那些散布在各处的回报,不管怎样,乍一看。

              所以让代码说话吧。当您看到长条件出现或嵌套 if 语句时,它表示将这些东西分解为多个函数可能会更好,或者需要更优雅地重写它。

              【讨论】:

              • 懒惰和邋遢可能过于强烈。想想像断言这样的保护条件。如果它们不是真的,那么继续该方法中的其余代码是没有意义的。这不是懒惰或马虎。
              • 也许如果使用断言会更干净。这些 return X 语句是有争议的,因为它们导致一个函数的退出点超过 1 个。不幸的是,我还看到开发人员进一步将单个退出点保留在适当的位置,同时仍然使用 goto exit 代替各种语言(C、COBOL、Visual Basic)的这种保护条件。例如,通读 MySQL 源代码,您会在整个代码中看到这样的 goto some_exit_label 出现。
              • 虽然我不同意“懒惰和草率”,但您提出了一些很好的观点,即当函数过于复杂时“样式 2”如何快速发光。当然,这个答案不值得-4 ...你只是用“懒惰和马虎”惹恼了人们。说了这么多,风格#1 实际上在现代 OO 语言中更正确,因为资源清理——C 中风格 1 的主要 bugaboo——对 RAII 来说不是问题。这使得样式 1 有可能显着减少逻辑路径的数量。这已被证明可以减少错误数量。
              猜你喜欢
              • 2010-11-13
              • 2014-10-09
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-10-29
              • 2013-01-21
              相关资源
              最近更新 更多