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

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

有帮助吗?

解决方案

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

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

...可以变得更具可读性(恕我直言),如下所示:

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

    ...
}

所以是的,我认为函数/方法有多个“退出点”是可以的。

其他提示

没有人提及或引用 代码完成 所以我会做的。

17.1 返回

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

用一个 返回 当它增强可读性时. 。在某些例程中,一旦您知道答案,您希望立即将其返回到调用例程。如果例程的定义方式不需要任何清理,则不立即返回意味着您必须编写更多代码。

我想说,任意决定多个退出点是非常不明智的,因为我发现该技术在实践中很有用 一遍又一遍地, ,其实我也经常 重构现有代码 为了清楚起见,多个出口点。我们可以这样比较这两种方法:-

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

我认为后者要清楚得多。据我所知,如今对多个出口点的批评是一个相当古老的观点。

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

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

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 条件和大括号正确匹配。

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

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

正如肯特·贝克 (Kent Beck) 在讨论保护条款时指出的那样 实施模式 使例程具有单一入口和出口点......

“是为了防止在同一例程中跳入和流出许多位置时的混乱。当应用于用大量全球数据编写的Fortran或Assembly语言程序时,这很有意义,甚至了解执行哪些语句也是艰苦的工作...使用小方法和大部分本地数据,它是不必要的保守。”

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

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

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

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

在此之上

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

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

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

我在 C++ 的编码标准中看到了它是 C 的后遗症,就好像您没有 RAII 或其他自动内存管理一样,那么您必须为每次返回进行清理,这意味着剪切和粘贴清理或 goto(逻辑上与托管语言中的“finally”相同),两者都被认为是不好的形式。如果您的实践是在 C++ 或其他自动内存系统中使用智能指针和集合,那么就没有充分的理由,它完全取决于可读性,更多的是判断调用。

我倾向于这样的想法:返回语句 中间 的功能都不好。您可以使用 returns 在函数顶部构建一些保护子句,当然可以告诉编译器在函数末尾返回什么内容,没有问题,但在 中间 函数的详细信息很容易被忽略,并且会使函数更难解释。

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

是的, , 有:

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

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

更新: :显然 MISRA 指南提倡单一退出, , 也。

需要明确的是,我并不是说这是 总是 多重回报是错误的。但考虑到其他等效的解决方案,有很多充分的理由选择单一回报的解决方案。

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

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

没有为什么 我们不再生活在 20 世纪 70 年代. 。如果您的函数足够长以至于多次返回会成为问题,那么它就太长了。

(更不用说带有例外的语言中的任何多行函数无论如何都会有多个退出点。)

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

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

看到这段代码,我立刻会问:

  • 'foo' 永远为空吗?
  • 如果是这样,有多少“DoStuff”客户端曾经使用空“foo”调用该函数?

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

  1. 检查是毫无意义的,因为它从来都不是真的(即。它应该是一个断言)
  2. 检查很少是正确的,因此最好更改这些特定的调用者函数,因为无论如何它们可能应该采取一些其他操作。

在上述两种情况下,可以使用断言重新编写代码,以确保“foo”永远不为空并且相关调用者发生变化。

还有另外两个原因(我认为针对 C++ 代码),多个存在实际上可以有一个 消极的 影响。它们是代码大小和编译器优化。

函数出口处范围内的非 POD C++ 对象将调用其析构函数。如果有多个 return 语句,则可能会出现作用域中存在不同对象的情况,因此要调用的析构函数列表也会不同。因此,编译器需要为每个 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() );
}

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

多个退出点使编译器试图检测这一点的工作变得复杂,并且至少对于相对较新的 VC++ 版本来说,在函数体具有多个返回的情况下不会发生优化。看 Visual C++ 2005 中的命名返回值优化 更多细节。

拥有单一出口点会减少 圈复杂度 因此, 理论上, ,降低了更改代码时将错误引入代码的可能性。然而,实践往往表明需要采取更务实的方法。因此,我倾向于目标是有一个出口点,但如果更具可读性,则允许我的代码有多个出口点。

我强迫自己只使用一个 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);
}

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

  • 创建了许多可重用的函数,
  • 使该函数更具人类可读性,并且
  • 函数的重点是为什么这些值是正确的。

我想说你应该有所需的数量,或者任何使代码更清晰的东西(例如 保护条款).

我个人从未听说/见过任何“最佳实践”说您应该只有一份返回声明。

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

我相信多次回报通常是好的(在我用 C# 编写的代码中)。单返回风格是 C 的延续。但您可能没有使用 C 进行编码。

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

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

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

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

我曾经谈过 我的博客上有更详细的内容.

对于单一退出点有好话要说,就像对于不可避免的事情也有不好的说法一样 “箭” 编程的结果。

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

这俩 斯巴达编程 “SSDLPedia”的文章和 单一功能出口点 “Portland Pattern Repository's Wiki”的文章对此有一些富有洞察力的论点。当然,还有这篇文章需要考虑。

例如,如果您确实想要一个退出点(在任何非启用异常的语言中),以便在一个地方释放资源,我发现仔细应用 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 非常容易单独测试。

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

简而言之;

  • 很少的回报比很多回报更好
  • 多一回胜过大箭,并且 保护条款 一般都可以。
  • 如果可能的话,例外可以/应该取代大多数“保护条款”。

你知道这句格言—— 情人眼里出西施.

有些人发誓 网豆 和一些由 智能IDEA, ,一些由 Python 和一些由 PHP.

在某些商店,如果您坚持这样做,您可能会失去工作:

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

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

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

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

我记得 20 年前,我的一位同事因为雇用了今天所谓的员工而被解雇 敏捷开发 战略。他有一个缜密的渐进计划。但他的经理对他大喊“你不能逐步向用户发布功能!你必须坚持 瀑布”他对经理的回应是,增量开发将更准确地满足客户的需求。他相信要根据客户的需求进行开发,但经理相信要根据“客户的要求”进行编码。

我们经常因破坏数据标准化而感到内疚, MVP多维控制器 边界。我们内联而不是构造函数。我们走捷径。

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

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

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

懒惰是一个好的程序员的优点。

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

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 语句中,该语句随后可被视为保护子句。同样,在一个人为的示例中,代码的意图并不明显,并且 一些函数() 可能有一些其他副作用,或者结果可以用于 // 其余代码....

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

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

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

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

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

我曾经使用过糟糕的编码标准,这些标准迫使你使用单一的退出路径,如果函数不是微不足道的,那么结果几乎总是非结构化的意大利面条——你最终会遇到很多中断和继续,这只会妨碍你。

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

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

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

如果您的函数具有多个返回值(或“res=”s),那么将其分解为多个具有单个退出点的较小函数通常是一个好主意。

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

当然,在某些情况下,使用多个返回可以使代码比没有它们的明显版本更清晰。有人可能会说,如果您的函数太复杂而无法在没有多个 return 语句的情况下理解,则需要进行更多返工,但有时对此类事情保持务实是有好处的。

如果您最终收到多次退货,则您的代码可能有问题。否则,我同意有时能够从子例程中的多个位置返回是很好的,特别是当它使代码更清晰时。

珀尔 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 }
  }
}

最好这样写

珀尔 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]
}

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

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

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

这可能是一个不寻常的观点,但我认为任何相信多个返回语句受到青睐的人都从未在仅支持 4 个硬件断点的微处理器上使用过调试器。;-)

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

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top