我一直听说单个出口点函数是一种糟糕的代码编写方法,因为您会失去可读性和效率。我从来没听过有人和对方争论过。
我原以为这和CS有关,但这个问题在CStheory StackExchange被否决了。
- 关于"保护声明"和"单功能退出点"范式可维护性的研究是否可能重复?或者一个方法的多个返回点是好的还是坏的?或者一个函数应该只有一个返回语句?
- 答案是没有一个总是正确的答案。我经常发现用多个出口编码更容易。我还发现(在更新上面的代码时)由于相同的多个出口,修改/扩展代码更加困难。我们的工作就是根据具体情况做出决定。当一个决定总是有一个"最佳"的答案时,我们就没有必要了。
- @法西斯模式已经删除了最后两个问题,以确保他们将不得不一次又一次地回答。
- 尽管这个问题中有"争论"这个词,但我真的不认为这是一个基于观点的问题。它和好的设计等很相关。我看没有理由关闭它,但是W/E。
- 一个单一的出口点简化了调试、读取、性能测量和调优、重构,这是客观的,具有重大意义。使用早期返回(在简单的参数检查之后)可以使这两种样式合理地混合在一起。考虑到单个退出点的好处,用返回值乱丢代码只是懒惰、草率、粗心的程序员的证据——至少可能不喜欢小狗。
有不同的思想流派,这在很大程度上取决于个人偏好。
一个是,如果只有一个出口点,那么就不那么容易混淆了——您有一条通过该方法的路径,并且您知道在哪里寻找出口。在不利的一面,如果使用缩进来表示嵌套,代码最终会大量缩进到右边,并且很难遵循所有嵌套范围。
另一种方法是,您可以检查前提条件并在方法开始时尽早退出,这样您就可以在方法体中知道某些条件是正确的,而不会使整个方法体向右缩进5英里。这通常会最小化您必须担心的范围的数量,这使得代码更容易执行。
第三,你可以从任何你想离开的地方离开。在过去,这会更加令人困惑,但现在我们有了语法着色的编辑器和编译器,可以检测不可访问的代码,这就更容易处理了。
我完全在中间阵营。执行单个退出点是一个无意义的甚至是适得其反的限制imho,而在一个方法中随机退出有时会导致难以遵循逻辑的混乱,在逻辑中很难看到给定的代码位是否会被执行。但是"选通"您的方法可以显著简化方法的主体。
- 在singe exit范式中,可以借助go to语句来避免深层嵌套。此外,还可以在函数的本地Error标签下执行一些后处理,这对于多个return是不可能的。
- 通常有一个好的解决方案,可以避免使用Go-To。我更喜欢"返回(fail(…)",并将共享清理代码放入fail方法中。这可能需要传入一些局部变量以释放内存等,但除非您使用的是性能关键的代码,否则这通常比goto imo更干净。它还允许多个方法共享类似的清理代码。
- 有一种基于客观标准的最佳方法,但我们可以同意,有一些思想流派(正确和错误),它归结为个人偏好(对正确方法的偏好或对正确方法的偏好)。
我的一般建议是,如果可行,返回语句应该位于具有任何副作用的第一个代码之前,或者位于具有任何副作用的最后一个代码之后。我会考虑:
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; |
如果某个条件应该阻止某个函数执行任何操作,那么我宁愿在该函数执行任何操作的点上方的某个位置提前退出该函数。不过,一旦职能部门采取了有副作用的行动,我宁愿从最底层返回,以明确所有的副作用都必须得到处理。
- 您的第一个例子是管理ok变量,它看起来像是我的单一返回方法。此外,if-else块可以简单地重写为:return ok ? 0 : ERR_NOT_OK;。
- 第一个例子在所有代码之前都有一个return。对于使用?:操作符,将其写在单独的行上可以使许多IDE更容易将调试断点附加到不正常的场景中。顺便说一句,"单个出口点"的真正关键在于理解,对于每个对正常函数的特定调用,出口点是调用后立即出现的点。现在程序员们认为这是理所当然的,但事情并非总是这样。在一些罕见的情况下,代码可能需要在没有堆栈空间的情况下通过,从而导致函数…
- …通过条件或计算的goto退出。一般来说,任何有足够资源可以用汇编语言以外的任何语言编程的东西都可以支持一个栈,但是我已经编写了一些必须在非常严格的约束条件下运行的汇编代码(在一种情况下,RAM的字节数减少到零),并且在这种情况下有多个出口点是很有帮助的。
- 所谓的更清晰的例子不那么清晰,也不容易阅读。一个出口点总是更容易阅读,更容易维护,更容易调试。
- @guidog:根据省略部分中出现的内容,这两种模式都可能更易于阅读。使用"return x;"可以清楚地表明,如果达到语句,返回值将为x。使用"result=x;"可以打开在返回结果之前其他内容可能会更改结果的可能性。如果事实上有必要更改结果,这可能很有用,但是检查代码的程序员必须检查代码,以查看结果如何更改,即使答案是"它不能"。
如果您觉得在一个函数中需要多个出口点,那么这个函数太大了,而且做得太多。
我建议阅读罗伯特·C·马丁的《清洁代码》一书中关于函数的章节。
本质上,您应该尝试用4行或更少的代码编写函数。
Mike Long博客上的一些注释:
- 函数的第一条规则:它们应该是小的
- 函数的第二条规则:它们应该小于
- if语句、while语句、for循环等中的块应为一行长
- …这一行代码通常是一个函数调用
- 缩进不应超过一个或两个级别
- 函数应该做一件事
- 函数语句都应该处于相同的抽象级别
- 函数的参数不应超过3个
- 输出参数是一种代码味道
- 将布尔标志传递到函数中是非常糟糕的。根据定义,您在函数中做两件事。
- 副作用是谎言。
- 4行?您编写什么代码来实现这种简单性?我真的怀疑Linux内核或Git会这样做。
- "将布尔标志传递到函数中是非常糟糕的。根据定义,你在做两件事——函数中的事情。不。。。这个布尔值可能只影响四行中的一行。另外,虽然我同意保持函数大小小,但是four有点太限制了。这应该被视为一个非常宽松的指导方针。
- @Jesse您可以创建两个函数,以布尔值导致的行为差异命名,并将共享体分解为第三个函数。find(bool caseSensitive)可以变成类似find()、findCaseSensitive()和readChars()的东西。
- @罗杰,我从来没有说过那是不可能的,我说过那不可能总是这样。
- 添加这样的限制将不可避免地导致代码混乱。更重要的是,方法要简洁明了,坚持只做它们应该做的事情,没有不必要的副作用。
- @Jesse"添加这样的限制不可避免地会导致代码混淆。"-我强烈反对。很容易说:"保持代码简洁明了",问题是如何做到的。如果您遵循这些指导原则,您会发现它强制您的代码是干净的…
- @阿尔法辛,请告诉我你如何保持一个函数,它必须做所有它自己的布尔逻辑下四行?if(condition){ //line one doSomething(params); //line two } else { //line three doSomethingElse(params); //line four } // oops
- @Jesse你同时做"某物"和"某物"这一事实违反了第六个要点:"函数应该做一件事":)第二,这正好是四行——所以没关系。第三,建议是"你应该试着写4行或更少代码的函数"——这并不意味着5-6行的方法不好。这些规则意味着您的代码应该被分解成可咀嚼的小函数,每个函数只专注于做一件事情,因此应该相对较短。你不觉得这合理吗?好消息:ianjoyner.name/no_return.html
- @阿尔法辛,你必须在某个地方做逻辑。我的例子是调用方法,它需要逻辑来知道为了完成它的一项任务,是否需要某些东西或其他东西。所以不,我没有违反任何规则。
- @Alfasin事实上,这就是我对这些约束if/else语句的确切观点,编程的一个不可分割的部分甚至无法完成。这太限制了。
- @阿尔法辛:从你的文章中,有时包含条件。
- @那么Jesse应该用一个单独的函数来包装;)
- @阿尔法辛,这正是我的例子,但有了这些限制,这是不可能的。
- @换句话说,我给出的代码是包装器函数的主体,由于约束过于严格,它不适合。完全没有理由实施如此严格的约束,它们最终会使代码变得过于复杂,完全破坏了它们被放置到位的目的。
- @Jesse我看不出将代码分解成更多的函数会"使代码过于复杂"。
- @阿尔法辛,我从来没有说过会的,但很好的草人。我说过,将每个函数限制为四行。尤其是当你不能实现一些基本的编程概念时,比如…然后…其他四行逻辑。
- @Jesse限制函数的大小,会迫使您不断地将其拆分为较小的块。遵循"只做一件事"也是一样的。这不是一根稻草-这是清洁代码的概念。
- 事实上,每当您计算代码行数时,您可能做了一些错误的事情。保持简短和甜蜜,是的…保持干净简洁,是的…一个方法执行一个任务,是的……限制一个函数中允许的代码行,不,不要这样做。
- 你继续回到"4行",那是"行动"的解释。从书中:"函数的第一条规则是它们应该是小的。函数的第二个规则是它们应该小于这个值。函数的长度不应为100行。函数不应该有20行长。"我希望20行能让你更快乐。
- @阿尔法辛,我不确定是什么让你困惑,但我不会就stackoverflow展开争论,因为你不明白这四行限制的简单证明是不可获得的。祝您今天过得愉快。
- @又是阿尔法辛……干得不错。我从一开始就很清楚,我不是在为100行函数辩护,我指出了这些限制中的缺陷,是的,我一直回到"4行",因为我的论点从一开始就是这样的。是你没有意识到这一点。
- 据记录,我很少写二十行函数。
- @杰西,你上次的评论让我更高兴。我认为我们毕竟是同一页。祝你过得愉快!
- @阿尔法辛是的,我也这么认为。我不提倡长时间的功能。我是说,不要把自己局限在不现实的事情上(本例中是4行),即使你有一个更现实的事情,比如20行,这也不是一个硬性和快速的规则。如果您遇到这样一种情况:要完成手头的单个任务,需要23行代码,而且没有一个很好的逻辑代码块可以完成一个好的功能,那么您稍微改变一下规则并编写23行函数。如果限制4行,你会经常打破这个规则,这就是我不喜欢它的原因。
- @杰西,我完全同意你的意见(你最后的评论)。我相信这些所谓的"规则"都不是"硬规则"——更像是"经验法则",是要瞄准的——但不是一成不变的。
- 请记住,行数作为指示符也是语言依赖的:例如,在Python中,它比Java更具表现力,如果编写20行函数,它被认为是比较长的,而在Java中,由于语言更冗长,它会更有意义。
- 是的,因为有一万种方法的物体要好得多。
- 这是一个罕见的答案,我希望我能投下多次。
- 这是愚蠢的。功能只有在您重用它们或计划与其他人共享时才有意义。如果将一个80行函数拆分为20个子函数,而这些子函数只被一个函数使用,您的收益是多少?
- @sarrrek当您向上拆分函数时,您可以命名单独的部分(在新函数名中)。您可以为在那里处理的值提供更具体的名称(因为函数参数名称可以不同于您调用函数时使用的名称)。您可以更清楚地显示不同部分中哪些值是相关的(因为您只将所需的值传递给新函数)。您可以避免深度嵌套(在循环中,您可以调用一个新函数,而不是使用一个新的嵌套代码块)。您可以使用较少的注释(注释会随着时间的推移而过时)。
- @Sarrrrek同样,当您拆分函数时,您可以单独对部分进行单元测试,这通常会减少编写量。更简单更准确的测试。您最终做的测试总体上更少(例如,3+3+3=9个单独的测试,而不是3*3*3=27个组合的测试)。
最重要的是,它取决于可交付成果的需求。在"过去"中,有多个返回点的意大利面条代码会导致内存泄漏,因为喜欢这种方法的编码人员通常不会很好地清除内存泄漏。有些编译器在返回过程中弹出堆栈时"丢失"了对返回变量的引用,这也是从嵌套范围返回时出现的问题。更普遍的问题是一个可重入代码,它试图使函数的调用状态与其返回状态完全相同。OOP的变异子违反了这一点,这个概念被搁置了。
有可交付成果,尤其是内核,需要多个出口点提供的速度。这些环境通常有自己的内存和过程管理,因此泄漏的风险最小化。
就个人而言,我喜欢有一个单一的退出点,因为我经常使用它在RETURN语句中插入断点,并执行代码检查代码如何确定该解决方案。我可以直接进入入口并单步执行,使用广泛嵌套和递归的解决方案。作为代码评审员,一个函数中的多个返回需要更深入的分析——因此,如果您这样做是为了加快实现速度,那么您就是在抢彼得来救保罗。在代码审查中需要更多的时间,从而使有效实施的假设无效。
——2美分
详情请参阅本文件:Nistir 5459
- multiple returns in a function requires a much deeper analysis只有当函数已经很大时(>1屏),否则分析就容易了
- 多次返回从来不会使分析更容易,只是oposite
- 链接已断开(404)。
- @fusi-在archive.org上找到它并在这里更新了链接
单入口和出口点是结构化编程的最初概念,而不是一步一步的意大利面条编码。人们相信多个出口点函数需要更多的代码,因为您必须对分配给变量的内存空间进行适当的清理。考虑这样一个场景:函数分配变量(资源),过早地离开函数,如果不进行适当的清理,将导致资源泄漏。此外,在每个出口之前构建清理将创建大量冗余代码。
在我看来,只在一个点上退出一个函数(或其他控制结构)的建议常常被夸大了。通常只给出两个退出的原因:
单出口代码据说更容易读取和调试。(我承认我不太考虑这个原因,但事实证明了这一点。更容易阅读和调试的是单入口代码。)
单出口代码链接和更干净的返回。
第二个原因很微妙,并且有一些优点,特别是当函数返回一个大的数据结构时。不过,我不会担心太多,除非…
如果你是一个学生,你想在班上获得高分。做老师喜欢做的事。从他的角度来看,他可能有一个很好的理由;所以,至少,你会了解他的观点。这本身就有价值。
祝你好运。
我曾经是单一出口风格的倡导者。我的推理主要来自于痛苦…
单出口更容易调试。
考虑到我们现在拥有的技术和工具,这是一个远远不合理的位置,因为单元测试和日志记录可能会使单出口变得不必要。也就是说,当您需要观察代码在调试器中的执行时,要理解和处理包含多个出口点的代码就要困难得多。
当您需要插入任务以检查状态时(在现代调试器中被watch表达式替换),这种情况就变得尤为明显。以隐藏问题或完全中断执行的方式更改控制流也太容易了。
在调试器中,单出口方法更容易单步执行,并且更容易在不破坏逻辑的情况下将它们分开。
答案取决于上下文。如果您正在创建一个GUI,并且有一个初始化API的函数,并在主界面开始时打开窗口,那么它将充满可能引发错误的调用,每个调用都会导致程序实例关闭。如果使用嵌套的if语句和缩进,代码很快就会向右倾斜。在每个阶段返回一个错误可能会更好,而且实际上更可读,同时也同样容易用代码中的几个标志进行调试。
但是,如果您正在测试不同的条件,并根据方法中的结果返回不同的值,那么最好使用单个出口点。我曾经在matlab中做过图像处理脚本的工作,它可能会变得非常大。多个退出点可能会使代码非常难以执行。switch语句更合适。
最好的办法是边走边学。如果你正在为某个东西编写代码,试着找到其他人的代码,看看他们是如何实现的。决定你喜欢哪一种,不喜欢哪一种。