是否有充分的理由说明在函数中只有一个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 doStuff(foo) {
    if (foo != null) return;
}

Or

function doStuff(foo) {
    if (foo !== null) {
        ...
    }
}

在foo != null之前不要调用doStuff

if(foo != null) doStuff(foo);

这要求每个调用站点确保在调用之前满足调用的条件。如果有多个调用站点,则该逻辑最好放在单独的函数中、待调用函数的方法中(假设它们是一级公民)或代理中。

关于函数是否在数学上是可证明的这个话题,要考虑逻辑而不是语法。如果一个函数有多个返回点,这并不意味着(默认情况下)它在数学上不可证明。

这可能是一个不寻常的观点,但我认为任何相信多个return语句是有利的人都从未在只支持4个硬件断点的微处理器上使用过调试器。:-)

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

在其他条件相同的情况下,单一出口点可以显著提高代码的可读性。 但有一个问题:流行的结构

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

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

如果你的函数有多个返回值(或“res=”s),最好将它分解成几个较小的函数,每个函数都有一个退出点。

I've seen it in coding standards for C++ that were a hang-over from C, as if you don't have RAII or other automatic memory management then you have to clean up for each return, which either means cut-and-paste of the clean-up or a goto (logically the same as 'finally' in managed languages), both of which are considered bad form. If your practices are to use smart pointers and collections in C++ or another automatic memory system, then there isn't a strong reason for it, and it become all about readability, and more of a judgement call.

有时出于性能考虑,这是必要的(我不想获取不同的缓存线,就像继续一样;有时)。

如果你不使用RAII分配资源(内存、文件描述符、锁等),那么多次返回很容易出错,而且肯定是重复的,因为释放需要手动执行多次,你必须仔细跟踪。

在这个例子中:

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

我会把它写成:

function() {
    HRESULT error = OPERATION1FAILED;//assume failure
    if(SUCCEEDED(Operation1())) {

        error = OPERATION2FAILED;//assume failure
        if(SUCCEEDED(Operation3())) {

            error = OPERATION3FAILED;//assume failure
            if(SUCCEEDED(Operation3())) {

                error = OPERATION4FAILED; //assume failure
                if(SUCCEEDED(Operation4())) {

                    error = S_OK;
                }
            }
        }
    }
    return error;
}

这当然看起来更好。

这在手动资源释放的情况下尤其有用,因为在哪里和哪些释放是必要的是相当直接的。如下例所示:

function() {
    HRESULT error = OPERATION1FAILED;//assume failure
    if(SUCCEEDED(Operation1())) {

        //allocate resource for op2;
        char* const p2 = new char[1024];
        error = OPERATION2FAILED;//assume failure
        if(SUCCEEDED(Operation2(p2))) {

            //allocate resource for op3;
            char* const p3 = new char[1024];
            error = OPERATION3FAILED;//assume failure
            if(SUCCEEDED(Operation3(p3))) {

                error = OPERATION4FAILED; //assume failure
                if(SUCCEEDED(Operation4(p2,p3))) {

                    error = S_OK;
                }
            }
            //free resource for op3;
            delete [] p3;
        }
        //free resource for op2;
        delete [] p2;
    }
    return error;
}

如果在没有RAII(忘记异常问题!)的情况下使用多个出口编写这段代码,则必须多次写入删除。如果你用}else{then 这有点难看。

但是RAII使得多个出口资源问题变得毫无意义。