关于编码风格:为什么函数只有一个退出点?

Why should a function have only one exit-point?

我一直听说单个出口点函数是一种糟糕的代码编写方法,因为您会失去可读性和效率。我从来没听过有人和对方争论过。

我原以为这和CS有关,但这个问题在CStheory StackExchange被否决了。


有不同的思想流派,这在很大程度上取决于个人偏好。

一个是,如果只有一个出口点,那么就不那么容易混淆了——您有一条通过该方法的路径,并且您知道在哪里寻找出口。在不利的一面,如果使用缩进来表示嵌套,代码最终会大量缩进到右边,并且很难遵循所有嵌套范围。

另一种方法是,您可以检查前提条件并在方法开始时尽早退出,这样您就可以在方法体中知道某些条件是正确的,而不会使整个方法体向右缩进5英里。这通常会最小化您必须担心的范围的数量,这使得代码更容易执行。

第三,你可以从任何你想离开的地方离开。在过去,这会更加令人困惑,但现在我们有了语法着色的编辑器和编译器,可以检测不可访问的代码,这就更容易处理了。

我完全在中间阵营。执行单个退出点是一个无意义的甚至是适得其反的限制imho,而在一个方法中随机退出有时会导致难以遵循逻辑的混乱,在逻辑中很难看到给定的代码位是否会被执行。但是"选通"您的方法可以显著简化方法的主体。


我的一般建议是,如果可行,返回语句应该位于具有任何副作用的第一个代码之前,或者位于具有任何副作用的最后一个代码之后。我会考虑:

1
2
3
4
5
6
7
  if (!argument)  // Check if non-null
    return ERR_NULL_ARGUMENT;
  ... process non-null argument
  if (ok)
    return 0;
  else
    return ERR_NOT_OK;

比:

1
2
3
4
5
6
7
8
9
  int return_value;
  if (argument) // Non-null
  {
    .. process non-null argument
    .. set result appropriately
  }
  else
    result = ERR_NULL_ARGUMENT;
  return result;

如果某个条件应该阻止某个函数执行任何操作,那么我宁愿在该函数执行任何操作的点上方的某个位置提前退出该函数。不过,一旦职能部门采取了有副作用的行动,我宁愿从最底层返回,以明确所有的副作用都必须得到处理。


如果您觉得在一个函数中需要多个出口点,那么这个函数太大了,而且做得太多。

我建议阅读罗伯特·C·马丁的《清洁代码》一书中关于函数的章节。

本质上,您应该尝试用4行或更少的代码编写函数。

Mike Long博客上的一些注释:

  • 函数的第一条规则:它们应该是小的
  • 函数的第二条规则:它们应该小于
  • if语句、while语句、for循环等中的块应为一行长
  • …这一行代码通常是一个函数调用
  • 缩进不应超过一个或两个级别
  • 函数应该做一件事
  • 函数语句都应该处于相同的抽象级别
  • 函数的参数不应超过3个
  • 输出参数是一种代码味道
  • 将布尔标志传递到函数中是非常糟糕的。根据定义,您在函数中做两件事。
  • 副作用是谎言。


最重要的是,它取决于可交付成果的需求。在"过去"中,有多个返回点的意大利面条代码会导致内存泄漏,因为喜欢这种方法的编码人员通常不会很好地清除内存泄漏。有些编译器在返回过程中弹出堆栈时"丢失"了对返回变量的引用,这也是从嵌套范围返回时出现的问题。更普遍的问题是一个可重入代码,它试图使函数的调用状态与其返回状态完全相同。OOP的变异子违反了这一点,这个概念被搁置了。

有可交付成果,尤其是内核,需要多个出口点提供的速度。这些环境通常有自己的内存和过程管理,因此泄漏的风险最小化。

就个人而言,我喜欢有一个单一的退出点,因为我经常使用它在RETURN语句中插入断点,并执行代码检查代码如何确定该解决方案。我可以直接进入入口并单步执行,使用广泛嵌套和递归的解决方案。作为代码评审员,一个函数中的多个返回需要更深入的分析——因此,如果您这样做是为了加快实现速度,那么您就是在抢彼得来救保罗。在代码审查中需要更多的时间,从而使有效实施的假设无效。

——2美分

详情请参阅本文件:Nistir 5459


单入口和出口点是结构化编程的最初概念,而不是一步一步的意大利面条编码。人们相信多个出口点函数需要更多的代码,因为您必须对分配给变量的内存空间进行适当的清理。考虑这样一个场景:函数分配变量(资源),过早地离开函数,如果不进行适当的清理,将导致资源泄漏。此外,在每个出口之前构建清理将创建大量冗余代码。


在我看来,只在一个点上退出一个函数(或其他控制结构)的建议常常被夸大了。通常只给出两个退出的原因:

  • 单出口代码据说更容易读取和调试。(我承认我不太考虑这个原因,但事实证明了这一点。更容易阅读和调试的是单入口代码。)
  • 单出口代码链接和更干净的返回。
  • 第二个原因很微妙,并且有一些优点,特别是当函数返回一个大的数据结构时。不过,我不会担心太多,除非…

    如果你是一个学生,你想在班上获得高分。做老师喜欢做的事。从他的角度来看,他可能有一个很好的理由;所以,至少,你会了解他的观点。这本身就有价值。

    祝你好运。


    我曾经是单一出口风格的倡导者。我的推理主要来自于痛苦…

    单出口更容易调试。

    考虑到我们现在拥有的技术和工具,这是一个远远不合理的位置,因为单元测试和日志记录可能会使单出口变得不必要。也就是说,当您需要观察代码在调试器中的执行时,要理解和处理包含多个出口点的代码就要困难得多。

    当您需要插入任务以检查状态时(在现代调试器中被watch表达式替换),这种情况就变得尤为明显。以隐藏问题或完全中断执行的方式更改控制流也太容易了。

    在调试器中,单出口方法更容易单步执行,并且更容易在不破坏逻辑的情况下将它们分开。


    答案取决于上下文。如果您正在创建一个GUI,并且有一个初始化API的函数,并在主界面开始时打开窗口,那么它将充满可能引发错误的调用,每个调用都会导致程序实例关闭。如果使用嵌套的if语句和缩进,代码很快就会向右倾斜。在每个阶段返回一个错误可能会更好,而且实际上更可读,同时也同样容易用代码中的几个标志进行调试。

    但是,如果您正在测试不同的条件,并根据方法中的结果返回不同的值,那么最好使用单个出口点。我曾经在matlab中做过图像处理脚本的工作,它可能会变得非常大。多个退出点可能会使代码非常难以执行。switch语句更合适。

    最好的办法是边走边学。如果你正在为某个东西编写代码,试着找到其他人的代码,看看他们是如何实现的。决定你喜欢哪一种,不喜欢哪一种。