如何防止箭头反模式

Kip*_*pie 51 c# design-patterns anti-patterns

我对如何最好地将我的代码重构为更具可读性的东西感到困惑.

考虑一下这段代码:

var foo = getfoo();
if(foo!=null)
{
    var bar = getbar(foo);
    if(bar!=null)
    {
        var moo = getmoo(bar);
        if(moo!=null)
        {
            var cow = getcow(moo);
            ...
        }
    }
}
return;
Run Code Online (Sandbox Code Playgroud)

如您所见,需要大量嵌套if块,因为每个嵌套块依赖于先前的值.

现在我想知道如何让我的代码在这方面更清洁一些.

我自己想到的一些选择是:

  • 在每个if子句后返回,这意味着我有多个地方可以保留我的方法
  • 抛出ArgumentNullExceptions,之后我将它们捕获到最后并将return语句放在我的finally子句中(或者在try/catch块之外)
  • 使用标签和 goto:

大多数这些选项对我来说似乎有点"脏",所以我想知道是否有一种很好的方法来清理我创建的这个混乱.

Ger*_*nck 45

我会去做多个return陈述.这使代码易于阅读和理解.

不要goto出于明显的原因使用.

不要使用异常,因为您正在进行的检查并不例外,这是您可以预期的,因此您应该考虑到这一点.针对异常的编程也是一种反模式.

  • 也许我被宠坏了,但如果我想得到一个Foo并且没有得到一个,我发现这是特殊的.我的意思是,我可以期待一切.如果我想打开一个文件,我可以期待它被锁定,但结果仍然与我的目标不同.因为我希望软件做我想做的事情,我认为这是一个例外.或者你的意思是try..catch应该被完全禁止,就像goto一样? (15认同)
  • 我没有看到任何明显的理由不使用`goto`.`return`是最好的,接着是`return + try + finally`,但这是确实情况,如果清理很复杂,`goto`是一个可接受的选项(在C++中RAII应该处理所有事情,但C#最终确实添加了缩进级别,因此不等同). (7认同)
  • 上面的示例显然是关于业务逻辑,以检查可能发生的情况.我不会在这里使用例外.您的示例是关于外部资源的行为不应该是应有的,这是例外. (5认同)
  • 应该注意为什么箭头图案也存在.通过仅维护一个return语句而不是多个语句,可以减少某些度量复杂性.如果您的组织正在使用这些度量,那么使用多个return语句将影响度量标准. (2认同)

rex*_*ghk 44

考虑将空检查反转为:

var foo = getfoo();
if (foo == null)
{
    return;
}
var bar = getbar(foo);
if (bar == null)
{
    return;
}
...etc
Run Code Online (Sandbox Code Playgroud)

  • 考虑不遵循@ LeeAllan的建议. (73认同)
  • 考虑不为一行条件使用括号,使用类似`if(foo == null)return;`的方法 (18认同)
  • +1,请使用括号.这是一个好习惯. (9认同)
  • @LeeAllan只是旁白,但有时使用的编码约定将决定是否可以在一行语句中使用括号. (6认同)
  • @LeeAllan我总是包含一行条件的括号.恕我直言,它更容易调试,特别是当你的代码中的缩进搞砸了... (3认同)
  • 他们被称为BRACES.括号是这些 - > [] (2认同)

Gol*_*rol 27

你可以链接表达式.赋值返回指定的值,因此您可以检查其结果.此外,您可以在下一个表达式中使用指定的变量.

一旦表达式返回false,其他表达式就不再执行,因为整个表达式将返回false(因为and操作).

所以,这样的事情应该有效:

Foo foo; Bar bar; Moo moo; Cow cow;

if( (foo = getfoo()) != null &&
    (bar = getbar(foo)) != null &&
    (moo = getmoo(bar)) != null &&
    (cow = getcow(moo)) != null )
{
  ..
}
Run Code Online (Sandbox Code Playgroud)

  • 如果代码更大且更复杂,您可以将if中的代码移动到单独的方法中.我认为这比使用多个单独的代码块更好的方法.特别是如果您想添加"评论分隔符",解释下一部分的作用,则表明您需要分解代码. (2认同)

use*_*557 25

这是少数情况之一,使用它是完全可以接受的(如果不是可取的话)goto.

在这样的函数中,通常会有中间分配的资源或状态更改,需要在函数退出之前撤消.

基于回归的解决方案(例如,rexcfnghk和Gerrie Schenck)的常见问题是您需要记住在每次返回之前撤消这些状态更改.这会导致代码重复,并打开微妙错误的大门,尤其是在较大的功能中. 不要这样做.


CERT 实际上建议采用基于的结构方法goto.

特别要注意取自他们的示例代码copy_processkernel/fork.c的Linux内核.该概念的简化版本如下:

    if (!modify_state1(true))
        goto cleanup_none;
    if (!modify_state2(true))
        goto cleanup_state1;
    if (!modify_state3(true))
        goto cleanup_state2;

    // ...

cleanup_state3:
    modify_state3(false);
cleanup_state2:
    modify_state2(false);
cleanup_state1:
    modify_state1(false);
cleanup_none:
    return;
Run Code Online (Sandbox Code Playgroud)

从本质上讲,这只是"箭头"代码的一个更易读的版本,它不使用不必要的缩进或重复代码.这个概念可以很容易地扩展到最适合您的情况.


作为最后一点,特别是关于CERT的第一个兼容示例,我只想补充一点,只要有可能,设计代码就更简单,以便可以一次性处理清理.这样,您可以编写如下代码:

    FILE *f1 = null;
    FILE *f2 = null;
    void *mem = null;

    if ((f1 = fopen(FILE1, "r")) == null)
        goto cleanup;
    if ((f2 = fopen(FILE2, "r")) == null)
        goto cleanup;
    if ((mem = malloc(OBJSIZE)) == null)
        goto cleanup;

    // ...

cleanup:
    free(mem); // These functions gracefully exit given null input
    close(f2);
    close(f1);
    return;
Run Code Online (Sandbox Code Playgroud)

  • 我希望人们在贬低之前真正阅读你的答案.我们的开发人员对GOTO做出了下意识的反应,我认为你能很好地解决这个问题. (4认同)
  • 我觉得这只适用于C(或其他语言没有exeptions)这感觉就像是一个C重新实现try/finally.你能说服我,这对于一个upvote更好(或至少是不同的)吗? (4认同)

Dmi*_*nko 16

首先你的建议(每个if子句后返回)是一个很好的出路:

  // Contract (first check all the input)
  var foo = getfoo();

  if (Object.ReferenceEquals(null, foo))
    return; // <- Or throw exception, put assert etc.

  var bar = getbar(foo);

  if (Object.ReferenceEquals(null, bar))
    return; // <- Or throw exception, put assert etc.

  var moo = getmoo(bar);

  if (Object.ReferenceEquals(null, moo))
    return; // <- Or throw exception, put assert etc.

  // Routine: all instances (foo, bar, moo) are correct (not null) and we can work with them
  ...
Run Code Online (Sandbox Code Playgroud)

第二种可能性(在你的情况下)是稍微修改你的getbar()以及getmoo()函数,使得它们在null输入时返回null,所以你将拥有

  var foo = getfoo();
  var bar = getbar(foo); // return null if foo is null
  var moo = getmoo(bar); // return null if bar is null

  if ((foo == null) || (bar == null) || (moo == null))
    return; // <- Or throw exception, put assert(s) etc.    

  // Routine: all instances (foo, bar, moo) are correct (not null)
  ...
Run Code Online (Sandbox Code Playgroud)

第三种可能性是在复杂的情况下你可以使用Null Object Desing Patteren

http://en.wikipedia.org/wiki/Null_Object_pattern

  • Null对象模式需要小心; 它并不总是合适的,并且当您消除运行时异常时,如果您依赖于实际执行某些操作的对象,您仍会遇到意外行为.我发现NOP最适合你想要插入可选逻辑的情况,但不是你必须做某事的地方. (3认同)
  • +1 Null Objects模式.习惯这很棘手,但如果你正确应用它,它肯定可以简化这样的代码. (2认同)

Old*_*eon 11

做老派:

var foo;
var bar;
var moo;
var cow;
var failed = false;
failed = failed || (foo = getfoo()) == null;
failed = failed || (bar = getbar(foo)) == null;
failed = failed || (moo = getmoo(bar)) == null;
failed = failed || (cow = getcow(moo)) == null;
Run Code Online (Sandbox Code Playgroud)

更清楚 - 没有箭头 - 并且永远可以延伸.

请不要过去Dark Side使用gotoreturn.

  • 这是[Guard]概念的推导(http://en.wikipedia.org/wiki/Guard_%28computer_science%29). (3认同)
  • +1 imho是这里的最佳解决方案,但我使用`var validArgument = true`并使用`&=`operator +`!= null`检查,因为可读性表示你不想在以下if子句中否定 (3认同)
  • 这是正确的,因为当前写的,但其工作的原因不是逻辑运算符的最着名的特征(短路;如果OR的第一个表达式为真,或者对于AND为假,则第二个条件永远不会选中). (2认同)

Rex*_*err 5

var foo =                        getFoo();
var bar = (foo == null) ? null : getBar(foo);
var moo = (bar == null) ? null : getMoo(bar);
var cow = (moo == null) ? null : getCow(moo);
if (cow != null) {
  ...
}
Run Code Online (Sandbox Code Playgroud)

  • 这是迄今为止最易读的代码,但没有投票?! (3认同)