3

所以,这是另一个“好的”编程练习题。我确实搜索了一下,但这样的事情通常很难用几句话来定义。

问题是:从专业的角度来看,保持代码精简和简短(不一定更高效)或显式定义实例变量只是为了分配它们并立即返回是更好的编程实践吗?例如:

FILE * foo(){
    FILE * retVal; 
    foo2(); 
    retVal = foobar(); 
    return retVal;
}

从上面,我们可以立即看到foobar返回 a FILE *。因此,从这种编程风格中,我们可以更快地提取重要信息。与以下内容相比,这是正确的:

FILE * foo(){
    foo2(); 
    return foobar(); 
}

当然,它完成了同样的事情。但是,必须更深入地寻找才能找到相同的信息。我倾向于支持后一种编程风格,仅仅是因为它看起来更好。由于该程序运行方式的性质,我怀疑使用其中任何一种都会立即获得性能提升,因为任何一种选择仍然需要内存 - 区别在于用户或编译器是否分配它。

作为另一个保持代码简洁的例子:

int foo(){
    int i = 0;     
    while(foobar())
        i++:    
    return i;
}

TL:DR 问题>> 明确显示正在做的事情是否更好,或者是否可以,有利于简洁明了,缩短完成相同任务但不一定提供性能提升的代码?

4

6 回答 6

3

由于您计划的原因,准确和缩短代码之间的选择是主观的。在维护方面,我们大多数人更喜欢简短的代码。即使是学习者也更喜欢简短的代码,尽管这与他们必须喜欢的相反。

C 被设计为人类可读的,并且尽可能少地编译。它是程序性的,非常不华丽。编码有利于可读性和反对时间消耗的另一个理由。


您在示例中提供的两种方式都生成完全相同的 ASM 代码(注意-O)。

            .Ltext0:
                    .globl  foobar
                foobar:
                .LFB13:
                    .cfi_startproc
0000 B8000000       movl    $0, %eax
     00
0005 C3             ret
                    .cfi_endproc
                .LFE13:
                    .section    .rodata.str1.1,"aMS",@progbits,1
                .LC0:
0000 666F6F32       .string "foo2 called"
     2063616C 
     6C656400 
                    .text
                    .globl  foo2
                foo2:
                .LFB14:
                    .cfi_startproc
0006 4883EC08       subq    $8, %rsp
                    .cfi_def_cfa_offset 16
000a BF000000       movl    $.LC0, %edi
     00
000f E8000000       call    puts
     00
                .LVL0:
0014 B8000000       movl    $0, %eax
     00
0019 4883C408       addq    $8, %rsp
                    .cfi_def_cfa_offset 8
001d C3             ret
                    .cfi_endproc
                .LFE14:
                    .globl  foo
                foo:
                .LFB15:
                    .cfi_startproc
001e 4883EC08       subq    $8, %rsp
                    .cfi_def_cfa_offset 16
0022 B8000000       movl    $0, %eax
     00
0027 E8000000       call    foo2
     00
                .LVL1:
002c B8000000       movl    $0, %eax
     00
0031 4883C408       addq    $8, %rsp
                    .cfi_def_cfa_offset 8
0035 C3             ret
                    .cfi_endproc
                .LFE15:
                    .globl  main
                main:
                .LFB16:
                    .cfi_startproc
0036 4883EC08       subq    $8, %rsp
                    .cfi_def_cfa_offset 16
                .LBB8:
                .LBB9:
003a B8000000       movl    $0, %eax
     00
003f E8000000       call    foo2
     00
                .LVL2:
                .LBE9:
                .LBE8:
0044 B8000000       movl    $0, %eax
     00
0049 4883C408       addq    $8, %rsp
                    .cfi_def_cfa_offset 8
004d C3             ret
                    .cfi_endproc
                .LFE16:
                .Letext0:

..响应您的简约,微不足道的简短方式简洁方式


考虑到这一点,我可以自由地说,最好只是正确地应用两者。那就是.. 尽可能简洁明了

/* COMMENTED */
于 2015-06-12T13:22:06.513 回答
2

免责声明:以下内容均来自任何标准

通常,打开适当的优化后,编译器将优化大部分冗余部分,并使二进制文件尽可能高效。

牢记这一点,建议编写人类易于理解的代码。将优化部分(大部分)留给编译器。

编写人类更容易理解的代码

  • 更容易被别人接受
  • 更容易维护
  • 更容易调试
  • 最后但并非最不重要的,你的救星 双关语有趣的意思)
于 2015-06-12T13:12:01.200 回答
1

有可读性,有可调试性。

我会把你的例子(顺便说一下,它不会编译)写成

FILE* foo ()
{
    foo2(); 
    FILE* retVal = foobar(); 
    return retVal;
}

这样,如果我需要调试,我可以在 return 语句上设置一个断点,看看 retVal 是什么。避免过于复杂的表达式并使用中间变量通常也是一个好主意。一是便于调试,二是便于阅读。

于 2015-06-12T13:13:43.790 回答
0

我反对这种风格,尽管我知道很多人这样做是出于调试目的。

在典型的调试器中,可能很难看到立即返回的值,这可以被视为一种谬误。

如果你觉得你想这样做,我非常非常强烈地推荐两件事:

  • 使用 C99,因此您可以延迟声明。
  • 使用const.

所以我会这样写这个foo()例子,如果我必须:

FILE * foo(void)
{
  foo2();
  FILE * const retVal = foobar();
  return retVal;
}

请注意,const不能转到星号 ( const FILE *retVal = ...) 的左侧,因为那样会使类型变为const FILE *; 我们想要的是一个常量指针,而不是一个指向常量的指针。

ness的目的const是对人类读者说:“我在这里命名这个值,但这不是我要搞砸的状态”。

于 2015-06-12T13:24:24.943 回答
0

我同意代码可读。但是,我不同意第一个更容易阅读甚至维护。

  • 可读性:更多的代码可以阅读和理解。虽然对于这个例子来说这可能并不难,但它可能适用于更复杂的类型。
  • 可维护性:如果您更改返回类型,您也必须更改 retval 声明。

许多编码风格需要在块的开头定义变量。其中一些甚至只允许在功能级别开始。所以你在函数声明附近声明了变量,远离返回。

即使这是允许的:你得到了什么?它也可能在返回时隐藏强制,因为编译器也会抱怨错误的返回类型 - 如果您启用大多数警告将提高代码质量(如果认真对待的话)。

于 2015-06-12T13:44:04.230 回答
0

精简版

仅在添加不明显信息时才引入新变量。通常情况下,当变量用于替换有些复杂的表达式时

长版

和所有事情一样,这取决于。我认为解决这个问题的最佳方法是对情况进行成本效益分析。

使用中间变量的成本(纯粹从代码质量/理解的角度来看,我很确定任何体面的现代编译器都会优化这些变量)我会说解析变量的工作,理解上下文中的定义等等重要的是,将该变量的后一种使用与正在阅读您的代码的人在他的脑海中的程序工作模型联系起来。

好处是一个新元素,通过引入更多信息,可以帮助读者形成更简单的代码库心理模型,或者更精确的模型。而对于一个新的变量声明,大多数信息都包含在类型或名称中。

例如,考虑以下两个示例:

if(isSocial)
     return map[*std::min(d.begin(),d.end())].first;
else
     return map[*std::max(d.begin(),d.end())].first;
return idealAge;


if(isSocial)
     int closestPersonAge = map[*std::min(d.begin(),d.end())].first;
     idealAge = closestPersonAge
else
     int futhestPersonAge = map[*std::max(d.begin(),d.end())].first;
     idealAge = futhestPersonAge
return idealAge;

在第一个示例中,您的读者需要了解 std::min 的作用,'d' 和 'map' 是什么,它们的类型是什么,map 元素的类型等......在第二个示例中,通过提供有意义的变量名,你从本质上使读者不必理解计算,从而使他对代码有一个更简单的心理模型,同时大致保留相同数量的重要信息。

现在将其与:

int personAge = person.age();
return personAge;

在这种情况下,我认为 personAge 不会添加任何有意义的信息(变量和方法名称传达相同的信息),因此并不能真正帮助读者。

于 2015-06-12T14:30:56.280 回答