English 中文(简体)
函数应该只有一个返回语句吗?
原标题:
  • 时间:2008-08-31 09:26:55
  •  标签:
Locked. This question and its answers are locked because the question is off-topic but has historical significance. It is not currently accepting new answers or interactions.

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

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

最佳回答

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

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

…可以像这样变得更可读(IMHO):

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

    ...
}

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

问题回答

没有人提到或引用代码完成所以我会这么做。

17.1 return

尽量减少每个例程中的返回次数。如果在底部阅读时,你没有意识到它可能会返回到上面的某个地方,那么就很难理解一个例程。

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

我想说的是,武断地决定不使用多个退出点是非常不明智的,因为我发现这项技术在实践中是有用的。因此,我们可以比较这两种方法:-

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

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

结构化编程说每个函数只能有一个返回语句。这是为了限制复杂性。许多人,如Martin Fowler认为用多个返回语句编写函数更简单。他在经典的重构这本书是他写的。如果你遵循他的其他建议并编写小函数,这本书效果很好。我同意这一观点,只有严格的结构化编程纯粹主义者才坚持每个函数只返回一个语句。

正如Kent Beck在实现模式使例程具有单个入口和出口点。。。

"was to prevent the confusion possible when jumping into and out of many locations in the same routine. It made good sense when applied to FORTRAN or assembly language programs written with lots of global data where even understanding which statements were executed was hard work ... with small methods and mostly local data, it is needlessly conservative."

我发现用保护子句编写的函数比一长串嵌套的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++或另一个自动内存系统中使用智能指针和集合,那么没有充分的理由这样做,这一切都是为了可读性,更像是一个判断调用。

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

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

,有:

  • The single exit point gives an excellent place to assert your post-conditions.
  • Being able to put a debugger breakpoint on the one return at the end of the function is often useful.
  • Fewer returns means less complexity. Linear code is generally simpler to understand.
  • If trying to simplify a function to a single return causes complexity, then that s incentive to refactor to smaller, more general, easier-to-understand functions.
  • If you re in a language without destructors or if you don t use RAII, then a single return reduces the number of places you have to clean up.
  • Some languages require a single exit point (e.g., Pascal and Eiffel).

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

更新:显然MISRA指南也提倡单一出口

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

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

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

不,因为我们不再生活在20世纪70年代。如果你的函数足够长,以至于多次返回是个问题,那就太长了。

(除了一种语言中的任何多行函数(有例外)都会有多个出口点这一事实之外。)

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

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

看到这个代码,我会立即问:

  • Is foo ever null?
  • If so, how many clients of DoStuff ever call the function with a null foo ?

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

  1. the check is pointless as it never is true (ie. it should be an assertion)
  2. the check is very rarely true and so it may be better to change those specific caller functions as they should probably take some other action anyway.

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

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

函数出口处作用域中的非POD C++对象将调用其析构函数。如果有多个返回语句,则可能是作用域中有不同的对象,因此要调用的析构函数列表会有所不同。因此,编译器需要为每个返回语句生成代码:

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 
}

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

另一个问题与“命名返回值优化”(又名Copy Elision,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中的命名返回值优化

只有一个出口点可以减少循环复杂性,因此,理论上的降低了在更改代码时将错误引入代码的概率。然而,实践往往表明需要一种更实用的方法。因此,我倾向于有一个单一的退出点,但如果更可读的话,允许我的代码有几个。

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

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

(条件是绝对的…)

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

  • Multiple returns
  • Refactoring into separate functions

多次退货

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

诚然,它更长,也有点混乱,但在以这种方式重构函数的过程中,我们

  • created a number of reusable functions,
  • made the function more human readable, and
  • the focus of the functions is on why the values are correct.

我想说的是,您应该拥有所需数量的代码,或者任何使代码更干净的代码(例如保护子句)。

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

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

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

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

在C代码中,一种以上的返回样式可能是一个坏习惯,因为必须显式地取消资源分配,但Java、C#、Python或JavaScript等具有自动垃圾收集和<code>等构造的语言会尝试。。最后是块(以及在C#中使用的块),并且这个参数不适用——在这些语言中,需要集中的手动资源释放是非常罕见的。

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

因此,尽可能多地使用适合您艺术敏感性的返回,因为这是一个布局和可读性问题,而不是技术问题。

我已经谈到了在我的博客上详细介绍了这一点

关于拥有一个单一的出口点,有好的事情要说,就像关于不可避免的事情也有坏的事情要讲一样”arrow“结果编程。

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

两个斯巴达编程”SSDSLPedia“的文章和“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非常容易单独测试。

显然,在OO和启用异常的语言中,我不会使用这样的if语句(或者,如果我能毫不费力地完成它的话),代码会更简单。而且没有箭头。大多数非最终回报可能都是例外。

简言之

  • Few returns are better than many returns
  • More than one return is better than huge arrows, and guard clauses are generally ok.
  • Exceptions could/should probably replace most guard clauses when possible.

你知道一句格言:情人眼里出西施。

有些人发誓http://en.wikipedia.org/wiki/NetBeans“rel=”nofollow noreferrer“>NetBeans和IntelliJ IDEA,部分由Python和一些PHP

在一些商店里,如果你坚持这样做,你可能会失业:

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

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

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

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

我记得20年前,我的一位同事因为雇佣了今天的敏捷开发战略。他有一个细致的增量计划。但他的经理对他喊道:“你不能向用户增量发布功能!”!您必须坚持瀑布。”他对经理的回应是,增量开发将更精确地满足客户的需求。他相信开发是为了满足客户的需求,但经理相信编码是为了“客户的需求”。

我们经常因破坏数据规范化而感到内疚,MVPMVC边界。我们内联而不是构建函数。我们走捷径。

就我个人而言,我认为PHP是一种糟糕的做法,但我知道什么。所有的理论争论都归结为试图实现一套规则

quality = precision, maintainability and profitability.

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

Laziness is the virtue of a good programmer.

我倾向于使用guard子句来提前返回,否则在方法结束时退出。单一进入和退出规则具有历史意义,在处理一个具有多个返回(和许多缺陷)的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语句,然后可以将其视为一个保护子句。同样,在一个人为的例子中,代码的意图并不明显,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;
}

我能想到的一个很好的理由是代码维护:你有一个单一的出口点。如果您想更改结果的格式,。。。,它的实现要简单得多。此外,对于调试,您可以在那里粘贴一个断点:)

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

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

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

我使用过糟糕的编码标准,这些标准迫使你只能使用一条退出路径,结果几乎总是非结构化的意大利面条——如果函数一点也不琐碎的话——你最终会有很多中断和继续,这只是一种阻碍。

Single exit point - all other things equal - makes code significantly more readable. But there s a catch: popular construction

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

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

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

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

当然,在某些情况下,使用多次返回可以使代码比没有多次返回的明显版本更清晰。有人可能会说,如果你有一个函数太复杂,没有多次返回语句就无法理解,那么就需要更多的返工,但有时对这些事情要务实一些是好的。

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

Perl 6: Bad Example

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: Good Example

@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个硬件断点的微处理器上使用调试器

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

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

但是,事实并非如此,一个/多个返回语句并没有错。在某些语言中,这是一种比其他语言(C)更好的实践(C++)。





相关问题