编程风格:如果守卫条件不满足,是否应该提前返回?

32

我有时在想,下面两种方式哪一种更好的风格(如果有的话)?如果保护条件未满足,立即返回是否更好,还是只有当保护条件得到满足后才执行其他操作?

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

// 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;
}

5
我的经验法则:如果守卫条件失败了,就要尽早返回;在守卫条件之后(除非这会严重影响可读性),只有一个返回点。 - Bryan Anderson
12个回答

40

我更喜欢第一种风格,但是如果没有必要创建变量,我就不会这样做:

// Style 3
public SomeType aMethod() {

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

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

  return result;
}

1
考虑到它可以被设置为“null”,SomeType必须是某种指针类型,因此您可能意味着SomeType result = new SomeTypeInternal();,其中typedef SomeTypeInternal* SomeType;。话虽如此,直到您实际需要某些东西才延迟构建是一个非常好的想法。太多以C或Pascal开始的人忘记了在C++中可以在任何地方声明变量。 - Mike DeSimone
2
啊,我不用Java,所以我以为是C++。抱歉。问题上加个“Java”标签会更有帮助。 - Mike DeSimone
6
@Mike:实际上,正确的标签应该是 language-agnostic,这意味着问题不涉及指针语义或类似语言细节,而是关于在方法/函数中早返回还是晚返回的问题。 - stakx - no longer contributing
它不想将这个问题标记为Java,因为它不是关于Java的问题,我也不想限制受众。如果您愿意,可以随意编辑并用伪代码替换代码。 - John Topley
4
@Tim:语法可能是Java,但问题不仅限于Java。它涉及到一般的命令式编程。(它几乎立刻适用于C#、C和C++,稍微做一些语法上的调整后也适用于Python、Perl、Ruby和Tcl等语言...)它非常中立。 - Donal Fellows
显示剩余5条评论

29

在80年代晚期接受Jackson Structured Programming的培训后,我根深蒂固地相信“一个函数应该有一个单一入口和一个单一出口”的哲学,这意味着我按照Style 2编写代码。

然而,在过去几年中,我意识到用这种风格编写的代码通常过于复杂且难以阅读/维护,因此我转向使用Style 1。

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


哇,这让我想起了噩梦。谈论排除异常的概念。 - Mike DeSimone

16

风格1是Linux内核间接推荐的。

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

现在,有些人会声称,8个字符的缩进使代码移动得太远,使其在80个字符的终端屏幕上难以阅读。对此的答案是如果你需要超过3层缩进,那么你已经陷入了困境,应该修复你的程序。

风格2增加了缩进级别,因此不被鼓励使用。

个人而言,我也喜欢风格1。 风格2使在具有多个警卫测试的函数中匹配闭合括号更加困难。


6

我不确定“guard”是否是这里的正确词语。通常,不满足条件的保护会导致异常或断言。
但除此之外,我会选择样式1,因为在我看来,它可以使代码更加简洁。你有一个只有一个条件的简单示例。但如果有很多条件和样式2,会发生什么?这将导致许多嵌套的if或巨大的if条件(使用||&&)。我认为最好尽早从方法中返回,一旦你知道你可以这样做。
但这肯定是非常主观的 ^^


6

马丁·福勒(Martin Fowler)将这种重构称为:"使用守卫条款替换嵌套条件语句"

if/else语句还会带来圆形复杂度。因此,测试用例更难。为了测试所有的if/else块,您可能需要输入大量选项。

如果有任何守卫条款,您可以首先测试它们,并以更清晰的方式处理if/else子句中的真正逻辑。


5
如果你使用.net反编译器深入挖掘.net框架,你会发现.net程序员使用风格1(或者也许是unbeli提到的风格3)。 以上回答已经提到了原因,另外一个原因可能是为了使代码更易读、简洁和清晰。 这种风格最常用的情况是在检查输入参数时,如果你编写某种框架/库/DLL,你必须首先检查所有输入参数,然后再使用它们。

4
有时候这取决于使用的语言以及使用的"资源"类型(例如打开文件句柄)。
在C语言中,第二种风格明显更安全、更方便,因为函数必须在执行期间关闭和/或释放任何获得的资源。这包括已分配的内存块、文件句柄、操作系统资源的句柄(如线程或绘图上下文)、互斥锁上的锁定以及任何其他事物。将return延迟到最后或限制从函数中退出的次数可以让程序员更轻松地确保自己正确清理,有助于防止内存泄漏、句柄泄漏、死锁等问题。
在C++中,使用RAII风格编程,两种风格同样安全,因此您可以选择更方便的一种。我个人使用带有RAII-style的C++的第一种风格。没有RAII的C++就像C一样,所以在那种情况下,第二种风格可能更好。
在像Java这样带有垃圾回收功能的语言中,运行时会帮助平衡两种风格之间的差异,因为它会自我清理。但是,如果您没有显式“关闭”某些类型的对象,则这些语言也可能存在微妙的问题。例如,如果您构造了一个新的java.io.FileOutputStream,并且在返回之前不close它,则相关的操作系统句柄将保持打开状态,直到运行时垃圾回收了已超出范围的FileOutputStream实例。这可能意味着另一个需要打开文件进行写入的进程或线程可能无法进行操作,直到FileOutputStream实例被收集。

2
在Java中,你应该使用finally来处理诸如关闭文件流之类的事情。 - Marcus Andrén

3

虽然这种做法违反了我所学的最佳实践,但当我有这样的条件时,我发现减少if语句的嵌套要好得多。我认为这样更容易阅读,尽管它存在于多个位置,但仍然非常容易调试。


谁说它“违背最佳实践”了? - Michael Borgwardt
1
说实话,我只是从老师那里听说过这个。我猜在这个领域里它并不是最佳实践。 - Adam Driscoll

1

我认为Style1变得更加常用是因为如果与小方法结合使用,这是最佳实践。

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

例如,如果您想从大方法中提取一系列代码,并且此方法具有两个退出点,则开始出现问题,很难自动完成。当我有一个写成Style1风格的大方法时,我通常会将其转换为Style2,然后提取方法,然后在每个方法中都应该有Style1代码。

因此,Style1是最好的,但与小方法兼容。 Style2不是很好,但如果您有大方法并且没有时间拆分,则建议使用。


我真的不明白,为什么这不是被接受的答案。到目前为止,它是唯一一个解决长函数和短函数问题的答案。其他所有答案都更多地基于个人偏好或缺乏对两种风格的任何论证。 - Trendfischer

0

我个人更喜欢使用方法1,因为它在逻辑上更易于阅读,而且逻辑上更类似于我们要做的事情。(如果发生了什么不好的事情,立即退出函数,不要通过,也不要收集200美元)

此外,大多数时候,您希望返回一个不是逻辑上可能的结果(即-1),以向调用函数的用户指示函数未能正确执行并采取适当的操作。这也更适合方法1。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接