我应该尽早从函数返回还是使用if语句?[关闭]


304

我经常用两种格式编写这种函数,我想知道一种格式是否比另一种格式更受青睐,为什么。

public void SomeFunction(bool someCondition)
{
    if (someCondition)
    {
        // Do Something
    }
}

要么

public void SomeFunction(bool someCondition)
{
    if (!someCondition)
        return;

    // Do Something
}

我通常使用第一个编码,因为这是我的大脑在编码时的工作方式,尽管我认为我更喜欢第二个编码,因为它可以立即处理所有错误,而且我觉得它更易于阅读


9
我这次讨论有点晚了,所以我不会回答。我也在两年前考虑过这一点:lecterror.com/articles/view/code-formatting-and-readability我发现第二个更易于阅读,修改,维护和调试。但这也许只是我自己的意思:)
汉尼拔·莱克特博士


4
现在,这个问题是基于观点的问题的一个很好的例子
Rudolf Olah 2015年

2
那么如果在一个或另一个方向上没有绝对证据怎么办?如果在一个方向和另一个方向上提供了足够的论据,并且答案是正确的,则进行表决-这将非常有用。我发现像这样的结局问题对本网站的价值有害。
gsf

9
我喜欢基于意见的问题和答案。他们告诉我大多数人更喜欢什么,这使我可以编写代码供他人阅读。
Zygimantas

Answers:


402

我喜欢第二种风格。首先获取无效的案例,或者简单地退出或根据情况引发异常,在其中放置空白行,然后添加方法的“真实”主体。我发现它更容易阅读。


7
Smalltalk称这些为“保护条款”。至少这就是肯特·贝克(Kent Beck)在Smalltalk最佳实践模式中所说的;我不知道这是否是常见的说法。
Frank Shearar

154
提早退出可以让您摆脱有限的思维障碍。:)
Joren

50
我以前曾听说过这种被称为“蹦床模式”的方法-在坏情况出现之前,先清除它们。
RevBingo

14
我什至会说这绝对是唯一正确的选择。
奥利弗·韦勒

38
另外,如果一开始就没有边框,您就不会增加缩进。
doppelgreener

170

绝对是后者。前者现在看起来还不错,但是当您获得更复杂的代码时,我无法想象有人会这样认为:

public int SomeFunction(bool cond1, string name, int value, AuthInfo perms)
{
    int retval = SUCCESS;
    if (someCondition)
    {
        if (name != null && name != "")
        {
            if (value != 0)
            {
                if (perms.allow(name)
                {
                    // Do Something
                }
                else
                {
                    reval = PERM_DENY;
                }
            }
            else
            {
                retval = BAD_VALUE;
            }
        }
        else
        {
            retval = BAD_NAME;
        }
    }
    else
    {
        retval = BAD_COND;
    }
    return retval;
}

public int SomeFunction(bool cond1, string name, int value, AuthInfo perms)
{
    if (!someCondition)
        return BAD_COND;

    if (name == null || name == "")
        return BAD_NAME;

    if (value == 0)
        return BAD_VALUE;

    if (!perms.allow(name))
        return PERM_DENY;

    // Do something
    return SUCCESS;
}

我完全承认,我从未理解过单个出口点的优势。


20
单个出口点的优点是...有一个出口点!在您的示例中,有几点可能会返回。使用更复杂的功能,当返回值的格式更改时,这可能会变成寻线点。当然,有时候强制单个出口点没有意义。
JohnL

71
@JohnL大函数是问题,不是多个出口点。除非您在额外的函数调用会极大地减慢代码速度的情况下工作,否则……
Dan Rosenstark,2010年

4
@Yar:是的,但重点是。我不会尝试转换任何人,而只是指出了尽可能减少出口点的优势(无论如何,Jason的例子有点像个稻草人)。只是在下一次您要弄清楚为什么SomeFunction有时返回奇数值时,也许您希望您可以在返回之前添加一个日志记录调用。如果只有一个虫子,调试起来会容易得多!:)
JohnL

76
@Jason Viers好像有return value;帮助!?!然后value = ...,您必须打六打,其缺点是您永远无法确定在此赋值和最终收益之间值不会改变。至少立即返回显然是什么也不会改变结果。
Sjoerd

5
@Sjoed:我在这里第二个Sjoerd。如果要记录结果,则可以登录到呼叫者站点。如果您想知道原因,则必须在每个出口点/分配处登录,因此这两个方面都相同。
Matthieu M.

32

这取决于 -通常,我不会不遗余力地尝试移动一堆代码以尽早脱离该功能-编译器通常会为我解决这个问题。话虽这么说,但是如果我需要顶部的一些基本参数,否则我将无法继续,我将尽早进行突破。同样,如果条件if在功能上产生了巨大的障碍,我也会因此而使其早日爆发。

就是说,如果一个函数在调用时需要一些数据,我通常会抛出一个异常(参见示例),而不是仅仅返回一个异常。

public int myFunction(string parameterOne, string parameterTwo) {
  // Can't work without a value
  if (string.IsNullOrEmpty(parameterOne)) {
    throw new ArgumentNullException("parameterOne");
  } 
  if (string.IsNullOrEmpty(parameterTwo)) {
    throw new ArgumentNullException("parameterTwo");
  }

  // ...      
  // Do some work
  // ...

  return value;
}

9
如果最终结果是可维护的,那么谁在乎选择哪种样式呢?
杰夫·西弗

3
@Jeff Siver-因此,为什么这往往是一个“圣战”风格的问题,最终归结为个人喜好和内部风格指南所说的一切。
rjzii

1
这里的主要收获是他抛出了异常而不是早日返回。返回值不应重新用于有效性检查。如果您有各种条件,并且想让使用该方法的代码知道失败的原因怎么办?突然,您可能会从一种方法返回实际的业务数据,什么也没有(空结果)或许多不同的字符串,代码,数字等。仅描述其失败原因。不用了,谢谢。
DanMan 2014年

我的编译器建议的圈复杂度如何?如果可以,最好不要嵌套代码吗?
l --''''''------------''''''''''''2016-4-1

24

我希望早日返回。

如果您有一个入口点和一个出口点,那么您始终必须一直在脑海中一直跟踪整个代码,直到出口点为止(您永远不知道其他代码是否会对结果产生其他影响,所以您必须跟踪直到存在)。您不必执行哪个分支即可确定最终结果。这很难遵循。

有了一个条目并存在多个条目,您将在获得结果时返回,而不必费心追踪所有结果,以至于没有人对它做任何其他事情(因为自从您返回以来,将不再有其他任何事情)。就像将方法主体拆分为更多的步骤一样,每个步骤都有可能返回结果或让下一步尝试运气。


13

在必须手动清理的C编程中,对于单点返回有很多说法。即使现在不需要清理某些内容,也有人可以编辑您的函数,分配一些内容并需要在返回之前进行清理。如果发生这种情况,遍历所有return语句将是一场噩梦。

在C ++编程中,您有析构函数,甚至还有作用域退出防护。所有这些都需要放在这里,以确保代码首先是异常安全的,因此可以很好地防止代码过早退出,因此这样做没有逻辑上的弊端,而纯粹是样式问题。

我对Java不够了解,是否会调用“最终”块代码以及终结器是否可以处理需要确保某些事情发生的情况。

C#我当然无法回答。

D语言为您提供了适当的内置范围退出保护,因此为提前退出做好了充分的准备,因此除了样式外,不应出现其他问题。

当然,函数起初应该没有那么长,并且如果您有一个巨大的switch语句,您的代码也可能会被分解。


1
C ++允许使用一种称为返回值优化的方法,该方法允许编译器实质上省略通常在您返回值时发生的复制操作。但是,在各种情况下都很难做到,多个返回值就是其中一种情况。换句话说,在C ++中使用多个返回值实际上会使代码变慢。这肯定在MSC上成立,并且考虑到用多个可能的返回值实现RVO是非常棘手的(如果不是不可能的话),这在所有编译器中都可能是一个问题。
伊蒙·纳邦

1
在C语言中,只需使用goto并且可能会有两点回报。示例(注释中不可能使用代码格式):foo() { init(); if (bad) goto err; bar(); if (bad) goto err; baz(); return 0; err: cleanup(); return 1; }
mirabilos 2014年

1
我不喜欢goto,而是选择“提取方法”。当您认为需要实现返回值变量或goto时,为了确保始终调用清除代码,这是一个气味,您需要将其分解为多个函数。这使您可以使用Guard子句和其他早期返回技术来简化复杂的条件代码,同时仍确保清理代码始终运行。
BrandonLWhite

“有人可能会编辑您的功能”-这对于决策来说是一个荒谬的解释。将来任何人都可以做任何事情。这并不意味着您今天应该做一些特定的事情来防止某人将来破坏事情。
维克多·亚雷玛

称为使您的代码可维护。在现实世界中,代码是出于商业目的而编写的,有时开发人员稍后需要对其进行更改。当时我写了这个答案,尽管我已经修补了CURL以引入缓存并回想起了
通俗

9

争取双赢的早期回报。它们看起来很丑陋,但是比大型if包装器丑陋得多,尤其是在要检查多个条件的情况下。


9

我都用。

如果DoSomething是3-5行代码,则使用第一种格式设置方法后代码看起来很漂亮。

但是,如果它的行数多于该行数,那么我更喜欢第二种格式。当左括号和右括号不在同一屏幕上时,我不喜欢。


2
美丽没办法!压痕太多!
JimmyKane

@JimmyKane缩进只能在3-5行中发生,尤其是当您每级需要2(?)行时,其余的行会缩进:一个用于控制结构和块的开始,一个用于块的结束。
Deduplicator

8

一次进入单次退出的一个经典原因是,否则,形式语义就变得丑陋不堪(同样,GOTO被认为是有害的)。

换句话说,如果只有1个返回,则更容易推断出软件何时退出例程。这也是反对例外的理由。

通常,我会尽量减少提前归还的方法。


但是对于正式的分析工具,您可以将外部函数与该工具所需的语义进行综合,并使代码易于阅读。
Tim Williscroft

@蒂姆 这取决于您要进行多少次餐饮以及您要分析的内容。如果编码人员对事物保持理智,我发现SESE相当可读。
保罗·内森

我对分析的态度受Self项目的优化影响。99%的动态方法调用可以静态解析和消除。这样您就可以分析一些漂亮的直线代码。作为一个工作的程序员,我可以可靠地假设我将要处理的大多数代码是由普通程序员编写的。因此,他们不会编写非常好的代码。
Tim Williscroft 2010年

@Tim:足够公平。尽管我确实不得不指出静态语言仍然发挥着重要作用。
保罗·内森

面对异常没有理由。这就是为什么单点退出在C语言中表现出色,却在C ++中不会给您带来任何好处的原因。
peterchen

7

就个人而言,我更喜欢在开始时进行通过/失败条件检查。这样一来,我就可以将大多数最常见的故障放在功能顶部,并将其余逻辑归为一组。


6

这取决于。

如果有一些明显的死角条件需要立即检查,则提前返回,这将使其余的功能变得毫无意义。*

如果函数更复杂并且否则可能具有多个退出点,则设置Retval +单返回(可读性问题)。

* 这通常可以指示设计问题。如果发现很多方法需要在运行其余代码之前检查某些外部/参数状态或类似状态,则可能是调用者应处理的事情。


6
当我编写可以共享的代码时,我的口头禅是“不承担任何责任。不信任任何人”。您应该始终验证输入以及您依赖的任何外部状态。最好抛出一个异常而不是破坏某些东西,因为有人给了您错误的数据。
TMN 2010年

@TMN:好点。
鲍比表

2
关键是在OO中抛出异常,而不是返回异常。多次返回可能是不好的,多次抛出异常并不一定是代码的味道。
Michael K 2010年

2
@MichaelK:如果无法满足后置条件,则方法应为异常。在某些情况下,方法应提早退出,因为即使在功能开始之前也已达到后置条件。例如,如果调用“设置控件标签”方法将控件的标签更改为Fred,则窗口的标签已经为Fred,并且将控件的名称设置为当前状态将强制重绘(虽然在某些情况下可能有用,但会(这很烦人),如果旧名称和新名称匹配,则尽早使用set-name方法是完全合理的。
超级猫

3

如果使用

在Don Knuth的关于GOTO的书中,我读到他在if语句中给出始终使最可能的条件始终存在的理由。假设这仍然是一个合理的想法(而不是出于对时代速度的单纯考虑)。我要说早期返回不是良好的编程习惯,尤其是考虑到这样的事实,即它们经常用于错误处理,除非您的代码更有可能失败而不是失败:-)

如果遵循上述建议,则需要将该返回值放在函数的底部,然后您甚至不愿在该函数处将其称为返回值,只需设置错误代码并返回两行即可。从而达到1入口1出口的理想状态。

Delphi特定...

我认为这对Delphi程序员来说是一种很好的编程习惯,尽管我没有任何证据。预D2009,我们没有一个原子的方法返回一个值,我们有exit;result := foo;,或者我们可以只是抛出异常。

如果您不得不替代

if (true) {
 return foo;
} 

对于

if true then 
begin
  result := foo; 
  exit; 
end;

您可能会讨厌看到每个功能的顶部,并且更喜欢

if false then 
begin
  result := bar;

   ... 
end
else
   result := foo;

并完全避免exit


2
对于较新的Delphis,可以将其简化为if true then Exit(foo);我经常使用该技术来首先初始化resultnilFALSE分别初始化,然后检查所有错误条件以及Exit;是否满足条件。result然后(通常)在方法末尾设置成功案例。
JensG 2014年

是的,我喜欢这个新功能,尽管看起来像是使Java程序员安心的糖果,接下来您会知道,他们将让我们在过程中定义变量。
彼得·特纳

和C#程序员。是的,老实说,我会发现确实有用,因为它减少了声明和使用之间的界限(IIRC甚至有一些度量标准,忘记了名称)。
JensG 2014年

2

我同意以下声明:

我个人是保护子句(第二个示例)的粉丝,因为它减少了函数的缩进。有些人不喜欢它们,因为它会导致该函数有多个返回点,但我认为使用它们会更清楚。

取自stackoverflow中的这个问题


+1对于保护条款。我也更喜欢积极
向上的

1

这些天来,我几乎完全将早期回报用于极端情况。我写这个

self = [super init];

if (self != nil)
{
    // your code here
}

return self;

self = [super init];
if (!self)
    return;

// your code here

return self;

但这没关系。如果您的函数中有多个嵌套级别,则需要将其嵌套。


我同意。缩进是导致阅读困难的原因。压痕越少越好。您的简单示例具有相同的缩进级别,但是第一个示例肯定会增长到需要更多脑力的更多缩进。
user441521

1

我更愿意写:

if(someCondition)
{
    SomeFunction();
}

2
ew 那是预验证吗?还是在验证专用的方法中DoSomeFunctionIfSomeCondition
STW 2010年

这如何使您的关注点分开?
justkt 2010年

2
通过将函数的实现(其依赖关系逻辑)置于外部来破坏封装。
TMN 2010年

1
如果此方法在公共方法中运行,并且SomeFunction()是同一类中的私有方法,则可能没问题。但是,如果其他人正在调用SomeFunction(),则必须在此处重复检查。我发现最好使每种方法都能完成其工作所需的一切,而没有其他人应该知道这一点。
Per Wiklander's

2
这绝对是罗伯特·C·马丁(Robert C. Martin)在“清洁代码”中提出的风格。函数只能做一件事。但是,肯特·贝克(Kent Beck)建议,在“实施模式”中,在OP中提出的两个选择中,第二个更好。
Scott Whitlock

1

像您一样,我通常会写第一个,但更喜欢最后一个。如果我有很多嵌套的支票,我通常会重构为第二种方法。

我不喜欢如何将错误处理从检查中移开。

if not error A
  if not error B
    if not error C
      // do something
    else handle error C
  else handle error B
else handle error A

我更喜欢这样:

if error A
  handle error A; return
if error B
  handle error B; return
if error C
  handle error C; return

// do something

0

顶部的条件称为“前提条件”。通过放置if(!precond) return;,您可以直观地列出所有前提条件。

使用较大的“ if-else”块可能会增加缩进开销(我忘记了有关三级缩进的引用)。


2
什么?您不能使用Java提前归还吗?C#,VB(.NET和6),以及显然的Java(我以为是,但是由于15年来我没有使用过该语言,因此不得不进行查找)都允许早期返回。因此,不要指责“强类型语言”没有此功能。stackoverflow.com/questions/884429/…–
ps2goat

-1

我宁愿保持if语句较小。

因此,请选择:

if condition:
   line1
   line2
    ...
   line-n

if not condition: return

line1
line2
 ...
line-n

我会选择您所说的“早期回报”。

请注意,我不在乎早期的回报或其他任何事情,我真的很想简化代码,缩短if语句的正文等。

嵌套的if和for和while都是可怕的,不惜一切代价避免使用它们。


-2

正如其他人所说,这取决于。对于返回值的小函数,我可以编写早期返回值。但是对于大型函数,我希望始终在代码中占有一席之地,因为我知道可以放置一些在返回之前将要执行的内容。


-2

我在功能级别上练习快速失败。它使代码保持一致和整洁(对我和我使用过的人而言)。因此,我总是早点回来。

对于某些经常检查的条件,如果使用AOP,则可以实现这些检查的各个方面。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.