【问题标题】:Should a function have only one return statement?一个函数应该只有一个返回语句吗?
【发布时间】:2010-09-07 09:26:54
【问题描述】:

是否有充分的理由说明在函数中只有一个 return 语句是一种更好的做法?

或者只要逻辑正确就可以从函数中返回,这意味着函数中可能有很多返回语句?

【问题讨论】:

  • 我不同意这个问题与语言无关。对于某些语言,多重返回比其他语言更自然和方便。与使用 RAII 的 C++ 函数相比,我更可能抱怨 C 函数的早期返回。
  • 这是密切相关的,有很好的答案:programmers.stackexchange.com/questions/118703/…
  • 语言无关?向使用函数式语言的人解释他必须为每个函数使用一个返回值:p

标签: language-agnostic coding-style


【解决方案1】:

我经常在一个方法的开头有几个语句来返回“简单”的情况。例如,这个:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

...可以像这样变得更具可读性(恕我直言):

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

所以是的,我认为从一个函数/方法中有多个“退出点”很好。

【讨论】:

  • 同意。尽管拥有多个退出点可能会失控,但我绝对认为这比将整个函数放在 IF 块中要好。尽可能多地使用 return 以保持代码可读性。
  • 这被称为“保护声明”,是 Fowler 的重构。
  • 当函数保持比较短的时候,按照函数的结构在中间附近有一个返回点并不难。
  • 一大堆 if-else 语句,每个语句都有返回值?那没什么。这样的事情通常很容易重构。 (至少更常见的带有结果变量的单出口变体是,所以我怀疑多出口变体会更加困难。)如果你想要一个真正的头痛,看看 if-else 和 while 循环的组合(由本地控制booleans),其中设置了结果变量,导致方法结束时的最终退出点。那是单一退出的想法变得疯狂了,是的,我说的是必须处理它的实际经验。
  • '想象一下:你需要在'DoStuff'函数的末尾执行“IncreaseStuffCallCounter”方法。在这种情况下你会怎么做? :)' -- DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
【解决方案2】:

没有人提到或引用Code Complete,所以我会这样做。

17.1 返回

尽量减少每个例程中的返回次数。如果在底部阅读例程时,您没有意识到它返回到上面某个地方的可能性,则更难理解例程。

在提高可读性时使用 return。在某些例程中,一旦您知道答案,就想立即将其返回给调用例程。如果例程以不需要任何清理的方式定义,则不立即返回意味着您必须编写更多代码。

【讨论】:

  • +1 表示“最小化”的细微差别,但不禁止多次退货。
  • “更难理解”是非常主观的,尤其是当作者没有提供任何经验证据来支持一般性主张时……必须在代码中的许多条件位置设置一个变量因为最终的 return 语句同样受到“不知道变量被分配到函数上方某处的可能性!
  • 就我个人而言,我倾向于尽可能早点返回。为什么?好吧,当您看到给定案例的 return 关键字时,您会立即知道“我完成了”——您不必继续阅读以弄清楚之后会发生什么(如果有的话)。
  • @HestonT.Holtmann:Code Complete 在编程书籍中的独特之处在于其建议 有经验证据支持。
  • 这应该是公认的答案,因为它提到拥有多个返回点并不总是好的,但有时是必要的。
【解决方案3】:

我会说任意决定反对多个出口点是非常不明智的,因为我发现该技术在实践中非常有用一遍又一遍,事实上我经常重构为清楚起见,将现有代码复制到多个退出点。我们可以这样比较这两种方法:-

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

将此与允许多个退出点的代码进行比较:-

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

我认为后者要清楚得多。据我所知,现在对多个出口点的批评是一种相当陈旧的观点。

【讨论】:

  • 清晰是旁观者的眼睛——我看着一个函数,寻找一个开始、中间和结束。当函数很小时它很好 - 但是当你试图找出为什么某些东西被破坏并且“其余代码”变得不平凡时,你可以花很多时间寻找为什么 ret 是
  • 首先,这是一个人为的例子。第二, :string ret;" 在第二个版本中去了哪里?第三,Ret 没有包含有用的信息。第四,为什么一个函数/方法中有这么多逻辑?第五,为什么不将 DidValuesPass( type res ) 然后 RestOfCode() 分开子功能?
  • @Rick 1. 根据我的经验,这实际上是我遇到过很多次的模式,2. 它是在“其余代码”中分配的,也许不清楚。 3. 嗯?是例子吗? 4. 好吧,我想这方面是人为的,但有可能证明这一点是合理的,5. 可以...
  • @Rick 的重点是,提前返回通常比将代码包装在一个巨大的 if 语句中更容易。根据我的经验,它出现了很多,即使进行了适当的重构。
  • 没有多个返回语句的意义在于使调试更容易,其次是为了可读性。最后的一个断点允许您查看退出值,无异常。至于可读性,显示的 2 个函数的行为不同。如果 !r.Passed 则默认返回为空字符串,但“更易读”的字符串将其更改为返回 null。作者误读了之前只有几行之后的默认值。即使在微不足道的示例中,也很容易出现不明确的默认返回,最后的单个返回有助于强制执行。
【解决方案4】:

我目前正在开发一个代码库,其中两个工作人员盲目地赞同“单点退出”理论,我可以根据经验告诉你,这是一种可怕的可怕做法。它使代码极难维护,我会告诉你原因。

使用“单点退出”理论,您不可避免地会得到如下所示的代码:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

这不仅使代码很难理解,而且现在说你需要返回并在 1 和 2 之间添加一个操作。你必须缩进整个该死的函数,祝你好运确保您的所有 if/else 条件和大括号都正确匹配。

这种方法使代码维护极其困难且容易出错。

【讨论】:

  • @Murph:如果不阅读每个条件,您将无法知道在每个条件之后不会发生任何其他事情。通常我会说这类话题是主观的,但这显然是错误的。每个错误都返回一个,你就完成了,你确切地知道发生了什么。
  • @Murph:我见过这种代码被使用、滥用和过度使用。这个例子很简单,因为里面没有真正的 if/else。这种代码需要分解的只是一个被遗忘的“其他”。 AFAIK,这段代码非常需要异常。
  • 你可以把它重构成这样,保持它的“纯度”: if(!SUCCEEDED(Operation1())) { }else error = OPERATION1FAILED; if(error!=S_OK){ if(SUCCEEDED(Operation2())) { } else error = OPERATION2FAILED; } if(error!=S_OK){ if(SUCCEEDED(Operation3())) { } else error = OPERATION3FAILED; } //等等。
  • 这段代码不只有一个退出点:每个“error =”语句都在退出路径上。这不仅仅是退出函数,而是退出任何块或序列。
  • 我不同意单次返回“不可避免地”会导致深度嵌套。您的示例可以编写为具有单个返回(没有 goto)的简单线性函数。如果您不或者不能完全依赖 RAII 进行资源管理,那么提前返回最终会导致泄漏或重复代码。最重要的是,早期的回报使得断言后置条件变得不切实际。
【解决方案5】:

Structured programming 说每个函数只应该有一个返回语句。这是为了限制复杂性。许多人(例如 Martin Fowler)认为编写具有多个返回语句的函数更简单。他在他写的经典refactoring 书中提出了这个论点。如果您遵循他的其他建议并编写小函数,这将很有效。我同意这个观点,只有严格的结构化编程纯粹主义者才会遵守每个函数的单个返回语句。

【讨论】:

  • 结构化编程什么也没说。一些(但不是全部)自称是结构化编程倡导者的人这么说。
  • "如果您遵循他的其他建议并编写小函数,这将非常有效。"这是最重要的一点。小函数很少需要很多返回点。
  • @wnoise +1 发表评论,如此真实。所有“结构编程”都说不要使用 GOTO。
  • @ceretullis:除非有必要。当然它不是必需的,但在 C 语言中很有用。Linux 内核使用它,并且有充分的理由。 GOTO 认为有害 谈到使用GOTO 来移动控制流,即使函数存在。它从不说“永远不要使用GOTO”。
  • “都是关于忽略代码的‘结构’。”——不,恰恰相反。 “说应该避免它们是有道理的”——不,它没有。
【解决方案6】:

正如 Kent Beck 在讨论 Implementation Patterns 中的保护子句时指出的那样,使例程只有一个入口和出口点......

"是为了防止可能的混淆 当跳进跳出许多 在同一例程中的位置。它使 应用于 FORTRAN 或 编写的汇编语言程序 有大量的全球数据,甚至 了解哪些陈述是 执行起来很辛苦……使用小方法和主要是本地数据,这是不必要的保守。”

我发现使用保护子句编写的函数比一长串嵌套的 if then else 语句更容易理解。

【讨论】:

  • 当然,“一大堆嵌套的 if-then-else 语句”并不是保护子句的唯一替代方案。
  • @AdrianMcCarthy 你有更好的选择吗?它会比讽刺更有用。
  • @kuhaku:我不确定我会称之为讽刺。答案表明这是一种非此即彼的情况:保护子句或长嵌套的 if-then-else 束。除了保护子句之外,许多(大多数?)编程语言提供了许多方法来分解这种逻辑。
【解决方案7】:

在没有副作用的函数中,没有充分的理由有多个返回值,您应该以函数式风格编写它们。在具有副作用的方法中,事情更具有顺序性(时间索引),因此您以命令式风格编写,使用 return 语句作为停止执行的命令。

换句话说,如果可能的话,喜欢这种风格

return a > 0 ?
  positively(a):
  negatively(a);

在此

if (a > 0)
  return positively(a);
else
  return negatively(a);

如果您发现自己编写了多层嵌套条件,则可能有一种方法可以重构它,例如使用谓词列表。如果您发现 if 和 else 在语法上相距甚远,您可能希望将其分解为更小的函数。跨越一屏文本的条件块很难阅读。

没有适用于所有语言的硬性规定。像只有一个 return 语句这样的东西不会让你的代码变好。但好的代码往往会让你以这种方式编写函数。

【讨论】:

  • +1 "如果你发现你的 if 和 else 在句法上相距甚远,你可能想把它分解成更小的函数。"
  • +1,如果这是一个问题,通常意味着你在一个函数中做了太多。这不是最高投票的答案,这让我很沮丧
  • Guard 语句也没有任何副作用,但大多数人会认为它们很有用。 因此,即使没有副作用,也可能有理由提前停止执行。 在我看来,这个答案并没有完全解决问题。
  • @MaartenBodewes-owlstead 见"Strictness and Laziness"
【解决方案8】:

我在 C++ 的编码标准中看到它是 C 的遗留问题,好像你没有 RAII 或其他自动内存管理,那么你必须为每次返回进行清理,这要么意味着削减 - and-paste 清理或 goto(逻辑上与托管语言中的“finally”相同),这两者都被认为是错误的形式。如果您的实践是在 C++ 或其他自动内存系统中使用智能指针和集合,那么就没有充分的理由这样做,而这一切都与可读性有关,更多的是一种判断调用。

【讨论】:

  • 说得好,虽然我确实相信在尝试编写高度优化的代码(例如软件蒙皮复杂的 3d 网格!)时复制删除会更好
  • 是什么让你相信这一点?如果您的编译器优化不佳,在取消引用 auto_ptr 时会产生一些开销,则可以并行使用普通指针。尽管首先使用非优化编译器编写“优化”代码会很奇怪。
  • 这是一个有趣的规则例外:如果您的编程语言不包含在方法结束时自动调用的内容(例如 Java 中的 try ... finally ) 并且您需要进行资源维护,您可以在方法结束时使用单个返回来完成。在你这样做之前,你应该认真考虑重构代码以摆脱这种情况。
  • @PeteKirkham 为什么 goto 清理不好?是的 goto 可以用不好,但是这种特殊用法还不错。
  • @q126y 在 C++ 中,与 RAII 不同,它在抛出异常时失败。在 C 中,这是一种完全有效的做法。见stackoverflow.com/questions/379172/use-goto-or-not
【解决方案9】:

我倾向于认为函数中间中的返回语句是不好的。您可以使用返回在函数顶部构建一些保护子句,当然可以告诉编译器在函数末尾返回什么而没有问题,但在函数的 middle 中返回可能很容易被遗漏,并且会使函数更难解释。

【讨论】:

    【解决方案10】:

    是否有充分的理由说明在函数中只有一个 return 语句是一种更好的做法?

    是的,有:

    • 单个退出点为断言后置条件提供了绝佳场所。
    • 能够在函数末尾的一个返回上放置一个调试器断点通常很有用。
    • 更少的回报意味着更少的复杂性。线性代码通常更容易理解。
    • 如果尝试将函数简化为单个返回会导致复杂性,那么这就是重构为更小、更通用、更易于理解的函数的动机。
    • 如果您使用的语言没有析构函数,或者您不使用 RAII,则单次返回可以减少您必须清理的地方的数量。
    • 某些语言需要一个退出点(例如 Pascal 和 Eiffel)。

    这个问题通常被认为是多重返回或深度嵌套的 if 语句之间的错误二分法。几乎总是有第三种解决方案,它是非常线性的(没有深度嵌套),只有一个出口点。

    更新:显然MISRA guidelines promote single exit也是。

    需要明确的是,我并不是说多次退货总是是错误的。但考虑到其他等效的解决方案,有很多充分的理由更喜欢单回报的解决方案。

    【讨论】:

    • 另一个很好的理由,可能是目前最好的理由,有一个单一的返回语句是日志记录。如果要向方法添加日志记录,可以放置一条日志语句来传达方法返回的内容。
    • FORTRAN ENTRY 语句有多常见?见docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html。如果您喜欢冒险,您可以使用 AOP 和事后建议记录方法
    • +1 前两点足以说服我。与倒数第二段相同。出于同样的原因,我不同意日志记录元素,因为我不鼓励深层嵌套条件,因为它们鼓励打破单一责任规则,这是将多态性引入 OOP 的主要原因。
    • 我想补充一点,对于 C# 和代码合同,后置条件问题不是问题,因为您仍然可以将 Contract.Ensures 与多个返回点一起使用。
    • @q126y:如果您使用goto 来获取常见的清理代码,那么您可能已经简化了函数,以便在清理结束时有一个return代码。所以你可以说你已经用goto 解决了这个问题,但我会说你通过简化为一个return 来解决它。
    【解决方案11】:

    具有单个退出点确实在调试中提供了优势,因为它允许您在函数末尾设置单个断点以查看实际返回的值。

    【讨论】:

    • 太棒了!您是唯一个人提及这个客观原因。这就是我更喜欢单个出口点而不是多个出口点的原因。如果我的调试器可以在 any 退出点设置断点,我可能更喜欢多个退出点。我目前的观点是,编写多个出口点的人为了自己的利益而这样做,而牺牲了那些必须在他们的代码上使用调试器的其他人(是的,我说的是所有编写代码的开源贡献者)多个出口点。)
    • 是的。我正在将日志记录代码添加到在生产中间歇性行为不端的系统(我无法单步执行)。如果以前的编码器使用单出口,那会容易得多。
    • 没错,在调试时它很有帮助。但在实践中,我能够在大多数情况下在调用函数中设置断点,就在调用之后 - 有效地得到相同的结果。 (当然,这个位置是在调用堆栈上找到的。)YMMV。
    • 除非你的调试器提供了一个 step-out 或 step-return 函数(据我所知,每个调试器都会这样做),它会在 after之后显示返回值> 返回。如果没有将其分配给变量,那么之后更改该值可能会有点棘手。
    • 我很久没有看到调试器不允许你在方法的“关闭”处设置断点(结束,右大括号,无论你的语言)并点击断点,无论方法中的位置或多少返回 statemet。此外,即使您的函数只有一个返回值,这并不意味着您不能以异常(显式或继承)退出该函数。所以,我认为这不是一个有效的观点。
    【解决方案12】:

    一般来说,我尝试从一个函数中只设置一个退出点。然而,有时这样做实际上最终会创建一个比必要的更复杂的函数体,在这种情况下,最好有多个退出点。它确实必须是基于产生的复杂性的“判断调用”,但目标应该是在不牺牲复杂性和可理解性的情况下尽可能少的退出点。

    【讨论】:

    • “一般来说,我试图让一个函数只有一个退出点”——为什么? “目标应该是尽可能少的退出点”——为什么?为什么有 19 人投票赞成这个不回答?
    • @JimBalter 归根结底,归结为个人喜好。更多的退出点通常会导致方法更复杂(尽管并非总是如此),并使人们更难理解。
    • " 归结为个人喜好。" -- 换句话说,你不能提供理由。 “更多的出口点通常会导致更复杂的方法(尽管并非总是如此)”——不,实际上,他们没有。给定两个逻辑上等价的函数,一个带有保护子句,一个带有单出口,后者将具有更高的圈复杂度,大量研究表明代码更容易出错且难以理解。您会从这里阅读其他回复中受益。
    【解决方案13】:

    不,因为we don't live in the 1970s any more。如果你的函数足够长以至于多次返回是个问题,那就太长了。

    (除此之外,语言中的任何多行函数都会有多个退出点。)

    【讨论】:

      【解决方案14】:

      我更喜欢单次退出,除非它真的使事情复杂化。我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:

      public void DoStuff(Foo foo)
      {
          if (foo == null) return;
      }
      

      看到这段代码,我立马问:

      • 'foo' 是否为空?
      • 如果是这样,有多少 'DoStuff' 的客户曾使用 null 'foo' 调用该函数?

      根据这些问题的答案,可能是这样的

      1. 检查毫无意义,因为它永远不会是真的(即它应该是一个断言)
      2. 检查很少是真的,因此最好更改那些特定的调用者函数,因为它们可能应该采取一些其他措施。

      在上述两种情况下,代码可能都可以通过断言重新编写,以确保 'foo' 永远不会为 null 并且相关的调用者已更改。

      还有另外两个原因(我认为具体到 C++ 代码)存在多个实际上会产生负面影响。它们是代码大小和编译器优化。

      函数出口处范围内的非 POD C++ 对象将调用其析构函数。在有多个 return 语句的情况下,范围内可能存在不同的对象,因此要调用的析构函数列表会有所不同。因此编译器需要为每个返回语句生成代码:

      void foo (int i, int j) {
        A a;
        if (i > 0) {
           B b;
           return ;   // Call dtor for 'b' followed by 'a'
        }
        if (i == j) {
           C c;
           B b;
           return ;   // Call dtor for 'b', 'c' and then 'a'
        }
        return 'a'    // Call dtor for 'a'
      }
      

      如果代码大小是一个问题 - 那么这可能是值得避免的事情。

      另一个问题与“命名返回值优化”(又名复制省略,ISO C++ '03 12.8/15)有关。如果可以,C++ 允许实现跳过调用复制构造函数:

      A foo () {
        A a1;
        // do something
        return a1;
      }
      
      void bar () {
        A a2 ( foo() );
      }
      

      只要按原样,对象'a1'在'foo'中构造,然后将调用其复制构造来构造'a2'。但是,复制省略允许编译器在堆栈上与“a2”相同的位置构造“a1”。因此,函数返回时无需“复制”对象。

      多个退出点使编译器在尝试检测这一点时的工作变得复杂,并且至少对于相对较新的 VC++ 版本,在函数体有多个返回的情况下没有进行优化。详情请见Named Return Value Optimization in Visual C++ 2005

      【讨论】:

      • 如果您从 C++ 示例中取出除最后一个 dtor 之外的所有 dtor,则在 if 语句的范围结束时仍必须生成销毁 B 以及随后的 C 和 B 的代码,因此您没有多重回报真的一无所获。
      • +1 在列表的底部,我们有 这种编码实践存在的真正原因 - NRVO。但是,这是一个微优化;并且,像所有微优化实践一样,可能是由一些 50 岁的“专家”开始的,他们习惯于在 300 kHz PDP-8 上进行编程,并且不了解干净和结构化代码的重要性。一般来说,只要有必要,请采纳 Chris S 的建议并使用多个 return 语句。
      • 虽然我不同意您的偏好(在我看来,您的 Assert 建议也是一个返回点,在这种情况下,C# 中的 throw new ArgumentNullException() 也是如此),我真的很喜欢您的其他考虑,它们是对我来说都是有效的,并且在某些利基环境中可能很关键。
      • 这里塞满了稻草人。为什么foo被测试的问题与主题无关,是做if (foo == NULL) return; dowork; 还是if (foo != NULL) { dowork; }
      【解决方案15】:

      只有一个退出点会减少Cyclomatic Complexity,因此,理论上会降低您在更改代码时将错误引入代码的可能性。然而,实践往往表明需要一种更务实的方法。因此,我的目标是拥有一个退出点,但如果这样更具可读性,则允许我的代码拥有多个退出点。

      【讨论】:

      • 非常有见地。虽然,我觉得在程序员知道何时使用多个退出点之前,它们应该被限制为一个。
      • 并非如此。 “if (...) return; ... return;”的圈复杂度与“if (...) {...} return;”相同。它们都有两条路径通过它们。
      【解决方案16】:

      我强迫自己只使用一个return 语句,因为它在某种意义上会产生代码异味。让我解释一下:

      function isCorrect($param1, $param2, $param3) {
          $toret = false;
          if ($param1 != $param2) {
              if ($param1 == ($param3 * 2)) {
                  if ($param2 == ($param3 / 3)) {
                      $toret = true;
                  } else {
                      $error = 'Error 3';
                  }
              } else {
                  $error = 'Error 2';
              }
          } else {
              $error = 'Error 1';
          }
          return $toret;
      }
      

      (条件随意……)

      条件越多,函数越大,越难阅读。因此,如果您适应了代码气味,您就会意识到这一点,并想要重构代码。两种可能的解决方案是:

      • 多次退货
      • 重构为单独的函数

      多次退货

      function isCorrect($param1, $param2, $param3) {
          if ($param1 == $param2)       { $error = 'Error 1'; return false; }
          if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
          if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
          return true;
      }
      

      分离功能

      function isEqual($param1, $param2) {
          return $param1 == $param2;
      }
      
      function isDouble($param1, $param2) {
          return $param1 == ($param2 * 2);
      }
      
      function isThird($param1, $param2) {
          return $param1 == ($param2 / 3);
      }
      
      function isCorrect($param1, $param2, $param3) {
          return !isEqual($param1, $param2)
              && isDouble($param1, $param3)
              && isThird($param2, $param3);
      }
      

      当然,它更长而且有点乱,但是在以这种方式重构函数的过程中,我们已经

      • 创建了许多可重用的函数,
      • 使函数更易于阅读,并且
      • 函数的重点是为什么值是正确的。

      【讨论】:

      • -1:不好的例子。您省略了错误消息处理。如果不需要,则 isCorrect 可以表示为 return xx && yy && zz;其中 xx、yy 和 z 是 isEqual、isDouble 和 isThird 表达式。
      【解决方案17】:

      我会说你应该有尽可能多的,或者任何使代码更清晰的东西(例如guard clauses)。

      我个人从未听过/见过任何“最佳做法”说您应该只有一份退货声明。

      在大多数情况下,我倾向于根据逻辑路径尽快退出函数(保护子句就是一个很好的例子)。

      【讨论】:

        【解决方案18】:

        我相信多次返回通常是好的(在我用 C# 编写的代码中)。单返回样式是从 C 中保留下来的。但是您可能不是在 C 中编码。

        没有法律要求所有编程语言中的方法只有一个退出点。有些人坚持这种风格的优越性,有时他们将其提升为“规则”或“法律”,但这种信念没有任何证据或研究支持。

        在 C 代码中使用多种返回样式可能是一个坏习惯,其中必须显式释放资源,但 Java、C#、Python 或 JavaScript 等具有自动垃圾回收和try..finally 等构造的语言块(和 C# 中的 using 块),并且此参数不适用 - 在这些语言中,需要集中手动释放资源是非常罕见的。

        在某些情况下,单个返回更具可读性,而在某些情况下则不是。看看它是否减少了代码行数,使逻辑更清晰或减少了大括号和缩进或临时变量的数量。

        因此,根据您的艺术感受使用尽可能多的回报,因为这是布局和可读性问题,而不是技术问题。

        我已经谈到了this at greater length on my blog

        【讨论】:

          【解决方案19】:

          有一个单一的退出点有好处,正如不可避免的"arrow" 编程产生的坏话一样。

          如果在输入验证或资源分配期间使用多个退出点,我会尝试将所有“错误退出”非常明显地放在函数的顶部。

          “SSDSLPedia”的Spartan Programming 文章和“Portland Pattern Repository's Wiki”的the single function exit point 文章对此都有一些深刻的论据。当然,还有这篇文章要考虑。

          如果你真的想要一个退出点(在任何非异常启用的语言中),例如为了在一个地方释放资源,我发现仔细应用 goto 是好的;例如,请参阅这个相当人为的示例(压缩以节省屏幕空间):

          int f(int y) {
              int value = -1;
              void *data = NULL;
          
              if (y < 0)
                  goto clean;
          
              if ((data = malloc(123)) == NULL)
                  goto clean;
          
              /* More code */
          
              value = 1;
          clean:
             free(data);
             return value;
          }
          

          就我个人而言,一般来说,我不喜欢箭头编程多于不喜欢多个出口点,尽管正确应用时两者都很有用。当然,最好的办法是将您的程序结构为两者都不需要。将你的函数分解成多个块通常会有所帮助:)

          虽然这样做时,我发现我最终会得到多个退出点,如本例所示,其中一些较大的函数已分解为几个较小的函数:

          int g(int y) {
            value = 0;
          
            if ((value = g0(y, value)) == -1)
              return -1;
          
            if ((value = g1(y, value)) == -1)
              return -1;
          
            return g2(y, value);
          }
          

          根据项目或编码指南,大部分样板代码都可以用宏代替。作为旁注,以这种方式分解它使得函数 g0、g1、g2 非常容易单独测试。

          显然,在面向 OO 和启用异常的语言中,我不会使用这样的 if 语句(或者根本不会使用它,如果我可以毫不费力地摆脱它),并且代码会更加简单.并且非箭头。大多数非最终回报可能是例外。

          总之;

          • 很少有回报比很多回报好
          • 多一回总比大箭头好,guard clauses一般都可以。
          • 在可能的情况下,例外可以/应该替换大多数“保护条款”。

          【讨论】:

          • 示例在 y
          • opengroup.org/onlinepubs/009695399/functions/free.html "如果 ptr 是空指针,则不会发生任何操作。"
          • 不,它不会崩溃,因为将 NULL 传递给 free 是一个已定义的无操作。必须首先测试 NULL 是一个令人讨厌的常见误解。
          • “箭头”模式并非不可避免的替代方案。这是一种错误的二分法。
          【解决方案20】:

          您知道这句格言 - 情人眼中的美

          有些人发誓NetBeans,有些人发誓IntelliJ IDEA,有些人发誓Python,有些人发誓PHP

          如果你坚持这样做,你可能会失去工作:

          public void hello()
          {
             if (....)
             {
                ....
             }
          }
          

          问题在于可见性和可维护性。

          我沉迷于使用布尔代数来减少和简化逻辑以及状态机的使用。但是,过去的同事认为我在编码中使用“数学技术”是不合适的,因为它不可见且不可维护。那将是一个不好的做法。抱歉,我所使用的技术对我来说是非常可见和可维护的——因为当我六个月后回到代码时,我会清楚地理解代码,而不是看到一团糟的意大利面条。

          嘿伙计(就像以前的客户常说的那样)做你想做的事,只要你知道如何在我需要你修复它时修复它。

          我记得 20 年前,我的一位同事因为采用了今天称为agile development 的策略而被解雇。他有一个细致的增量计划。但他的经理对他大喊“你不能增量地向用户发布功能!你必须坚持使用waterfall。”他对经理的回应是,渐进式开发将更准确地满足客户的需求。他相信为客户的需求而开发,但经理相信编码是“客户的要求”。

          我们经常因打破数据规范化、MVPMVC 界限而感到内疚。我们内联而不是构造函数。我们走捷径。

          就个人而言,我认为 PHP 是不好的做法,但我知道什么。所有的理论论证归结为试图满足一套规则

          质量 = 精度、可维护性 和盈利能力。

          所有其他规则都淡入背景。当然,这条规则永远不会消失:

          懒惰是善的美德 程序员。

          【讨论】:

          • “嘿伙计(就像以前的客户常说的那样)做你想做的,只要你知道如何解决它,当我需要你修复它时。”问题:通常不是你“修复”它。
          • +1 在这个答案上,因为我同意你要去的地方,但不一定同意你到达那里的方式。我会争辩说,理解是有层次的。即员工 A 经过 5 年的编程经验和 5 年的公司工作后的理解与员工 B 的理解非常不同,员工 B 是刚从公司开始的大学毕业生。我的观点是,如果员工 A 是唯一可以理解代码的人,那么它是不可维护的,所以我们都应该努力编写员工 B 可以理解的代码。这就是软件的真正艺术所在。
          【解决方案21】:

          我倾向于使用保护子句提前返回,否则在方法结束时退出。单一进入和退出规则具有历史意义,并且在处理具有多个返回(和许多缺陷)的单个 C++ 方法运行到 10 A4 页的遗留代码时特别有用。最近,公认的良好做法是保持方法小,这使得多个出口对理解的阻碍更小。在从上面复制的以下 Kronoz 示例中,问题是 //Rest of code... 中发生了什么?:

          void string fooBar(string s, int? i) {
          
            if(string.IsNullOrEmpty(s) || i == null) return null;
          
            var res = someFunction(s, i);
          
            foreach(var r in res) {
                if(!r.Passed) return null;
            }
          
            // Rest of code...
          
            return ret;
          }
          

          我意识到这个例子有点做作,但我很想将 foreach 循环重构为一个 LINQ 语句,然后可以将其视为一个保护子句。同样,在一个人为的示例中,代码的意图并不明显,并且 someFunction() 可能有其他一些副作用,或者结果可能会在 // 其余代码中使用。 ..

          if (string.IsNullOrEmpty(s) || i == null) return null;
          if (someFunction(s, i).Any(r => !r.Passed)) return null;
          

          给出以下重构函数:

          void string fooBar(string s, int? i) {
          
            if (string.IsNullOrEmpty(s) || i == null) return null;
            if (someFunction(s, i).Any(r => !r.Passed)) return null;
          
            // Rest of code...
          
            return ret;
          }
          

          【讨论】:

          • C++没有例外吗?那你为什么要返回null而不是抛出异常表明参数不被接受呢?
          • 正如我所指出的,示例代码是从以前的答案 (stackoverflow.com/a/36729/132599) 复制而来的。原始示例返回空值并且重构以抛出参数异常对于我试图提出的观点或原始问题并不重要。作为一种良好的做法,是的,我通常会(在 C# 中)在保护子句中抛出 ArgumentNullException 而不是返回空值。
          【解决方案22】:

          我能想到的一个很好的理由是代码维护:您有一个单点退出。如果您想更改结果的格式,...,实现起来要简单得多。另外,为了调试,你可以在那里设置一个断点:)

          话虽如此,我曾经不得不在一个库中工作,那里的编码标准规定“每个函数一个返回语句”,我发现这非常困难。我写了很多数值计算代码,而且经常有“特殊情况”,所以代码最终很难理解......

          【讨论】:

          • 这并没有什么不同。如果更改返回的局部变量的类型,则必须修复对该局部变量的所有分配。无论如何,最好定义一个具有不同签名的方法,因为您还必须修复所有方法调用。
          • @MaartenBodewes-owlstead - 它可以有所作为。仅举两个示例,您不必必须修复对局部变量的所有分配或更改方法调用,该函数可能会将日期作为字符串返回(局部变量将是实际日期, 仅在最后时刻格式化为字符串),或者它可能返回一个十进制数并且您想要更改小数位数。
          • @nnnnnn 好的,如果您想对输出进行后期处理...但我只想生成一种新方法来进行后期处理,而不要理会旧方法。这只是稍微难以重构,但您必须检查其他调用是否与新格式兼容。但这仍然是一个正当的理由。
          【解决方案23】:

          对于足够小的函数来说,多个退出点是可以的——也就是说,一个可以在一个屏幕长度上完整查看的函数。如果一个冗长的函数同样包含多个退出点,则表明该函数可以进一步细分。

          也就是说我避免使用多个退出函数除非绝对必要。在更复杂的函数中,由于一些模糊的行中的一些杂散返回,我感到很痛苦。

          【讨论】:

            【解决方案24】:

            我曾使用过糟糕的编码标准,这些标准会强制您使用单一的退出路径,如果该功能不是微不足道的,那么结果几乎总是非结构化的意大利面条 - 您最终会遇到很多中断并继续,只是进入方式。

            【讨论】:

            • 更不用说必须告诉你的大脑跳过每个返回成功与否的方法调用前面的if 语句:(
            【解决方案25】:

            单一退出点 - 所有其他条件都相同 - 使代码的可读性显着提高。 但有一个问题:流行的建筑

            resulttype res;
            if if if...
            return res;
            

            是假的,“res=”并不比“return”好多少。它有一个 return 语句,但函数实际结束的地方有多个。

            如果您的函数具有多个返回(或“res=”),通常最好将其分解为具有单个退出点的几个较小的函数。

            【讨论】:

              【解决方案26】:

              我通常的策略是在函数末尾只有一个 return 语句,除非通过添加更多来大大降低代码的复杂性。事实上,我更喜欢 Eiffel,它通过没有 return 语句来强制执行唯一的 return 规则(只有一个自动创建的“result”变量来放入你的结果)。

              在某些情况下,通过多次返回可以使代码比没有它们的明显版本更清晰。有人可能会争辩说,如果您的函数过于复杂而无法在没有多个 return 语句的情况下理解,则需要进行更多的返工,但有时在这些事情上务实是件好事。

              【讨论】:

                【解决方案27】:

                如果您最终得到多个回报,则您的代码可能有问题。否则我会同意,有时能够从子例程中的多个位置返回是件好事,尤其是当它使代码更清晰时。

                Perl 6:不好的例子

                sub Int_to_String( Int i ){
                  given( i ){
                    when 0 { return "zero" }
                    when 1 { return "one" }
                    when 2 { return "two" }
                    when 3 { return "three" }
                    when 4 { return "four" }
                    ...
                    default { return undef }
                  }
                }
                

                这样写会更好

                Perl 6:很好的例子

                @Int_to_String = qw{
                  zero
                  one
                  two
                  three
                  four
                  ...
                }
                sub Int_to_String( Int i ){
                  return undef if i < 0;
                  return undef unless i < @Int_to_String.length;
                  return @Int_to_String[i]
                }
                

                请注意,这只是一个简单的示例

                【讨论】:

                • 好的 为什么这被否决了?它不像它不是一个意见。
                【解决方案28】:

                作为指导方针,我最后投票支持单次返回。这有助于常见的代码清理处理 ...例如,看看下面的代码...

                void ProcessMyFile (char *szFileName)
                {
                   FILE *fp = NULL;
                   char *pbyBuffer = NULL:
                
                   do {
                
                      fp = fopen (szFileName, "r");
                
                      if (NULL == fp) {
                
                         break;
                      }
                
                      pbyBuffer = malloc (__SOME__SIZE___);
                
                      if (NULL == pbyBuffer) {
                
                         break;
                      }
                
                      /*** Do some processing with file ***/
                
                   } while (0);
                
                   if (pbyBuffer) {
                
                      free (pbyBuffer);
                   }
                
                   if (fp) {
                
                      fclose (fp);
                   }
                }
                

                【讨论】:

                • 你投票支持单次返回 - 在 C 代码中。但是,如果您使用一种具有垃圾收集和 try..finally 块的语言进行编码,该怎么办?
                【解决方案29】:

                这可能是一个不寻常的观点,但我认为任何认为应该支持多个返回语句的人都不必在仅支持 4 个硬件断点的微处理器上使用调试器。 ;-)

                虽然“箭头代码”的问题是完全正确的,但在使用多个 return 语句时似乎会消失的一个问题是在您使用调试器的情况下。您没有方便的包罗万象的位置来放置断点以保证您将看到出口并因此看到返回条件。

                【讨论】:

                • 这只是一种不同的过早优化。您永远不应该针对特殊情况进行优化。如果你发现自己经常调试一段特定的代码,那么它的错误不仅仅是它有多少退出点。
                • 也取决于你的调试器。
                【解决方案30】:

                函数中的返回语句越多,该方法的复杂性就越高。如果您发现自己想知道是否有太多的 return 语句,您可能想问问自己该函数中是否有太多的代码行。

                但是,一个/多个 return 语句没有任何问题。在某些语言中,它是一种比其他语言 (C) 更好的做法 (C++)。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-04-02
                  • 2012-06-22
                  • 2019-09-03
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多