明确声明变量是否有助于可读性,这是一个好的实践吗?

3

所以,这又是一个关于“好”的编程实践问题。我确实搜索了一下,但是像这样的问题往往很难用几个词来定义。

对于这个问题:从专业角度来看,是将代码保持简洁而短小(不一定更高效),还是显式地定义实例变量仅为了分配它们并立即返回更好的编程实践? 例如:

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

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

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

当然,这两种方式都能达到同样的效果。然而,要找到相同的信息,必须深入研究。我倾向于后一种编程风格,仅仅因为它看起来更好。由于程序运行的本质,我怀疑使用任何一种方式都不会立即获得性能提升,因为内存对于任何一种选择都是必要的 - 差别在于用户或编译器是否分配它。
另一个将代码保持简洁的例子:
int foo(){
    int i = 0;     
    while(foobar())
        i++:    
    return i;
}

TL:DR问题 >> 显示正在执行的内容是否更好,还是为了简洁和简明起见缩短完成相同任务但不一定提供性能增益的代码?

在编写代码时,我们需要找到平衡点。即使代码可以缩短,也应该尽量明确地展示代码的意图,以提高可读性和可维护性。因此,在代码中显示正在执行的内容更好。

请注意:使用编译器优化后,两个代码可能会生成相同的汇编代码。因此,可能不会出现性能问题。 - Sourav Ghosh
5
一个重要的点 - 当你在调试器中逐步执行代码时,你可能想要检查这些变量,如果你使用更简洁的编码风格,这将不容易实现。 - Paul R
出于可读性的原因,我个人倾向于仅在其自身表达式语句上调用函数。当包含函数调用的函数非常短时(例如您的示例),我允许自己违反这种做法。 - ouah
使用-O2,gcc为这两个foo()函数生成完全相同的代码。一般来说,我更喜欢前者的风格,但在短小精悍的代码中,我会使用后者,例如:int XY_public_API(int x, inty) { return xy_private_api(x, y); }。因此,哪种风格更好取决于具体用例。与任何“编码风格”一样,没有通用的建议。 - P.P
中间变量的使用通常是为了使调试更容易。因为变量可以在调试器中显示、设置、应用写入停止等很多其他有用的操作。我尽量利用中间变量,因为(最终)代码需要维护,有了中间变量可以更容易地确定运行问题的原因。 - user3629249
6个回答

3
选择精确和缩短代码取决于您所投射的原因,这是主观的。 在维护方面,我们大多数人更喜欢简洁的代码。 即使是初学者也会更喜欢简洁的代码,尽管这与他们应该偏好的相反。
C语言旨在具有人类可读性,并尽可能少地编译。它是过程式的,非常简洁。这是支持可读性并反对时间消耗的另一个原因。
在示例中提供的两种方式生成的汇编代码完全相同(请注意-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 */

2
通常情况下,启用适当的优化后,编译器将优化掉大部分冗余或无用的部分,使二进制文件尽可能高效。
因此建议编写易于人类理解的代码,将(大多数)优化部分留给编译器处理。
编写更易于人类理解的代码有以下好处:
- 更容易被他人接受 - 更容易维护 - 更容易调试 - 最后但并非最不重要的,对您来说是一种救命之恩 双关语 有趣的意味在内)

3
同意,但实际上我觉得这个版本并没有更易读。如果我无法记住当前函数的返回类型,那么我会觉得我的工作不对。此外,这会使得更改返回类型变得更加困难,特别是在强制转换规则下。 - too honest for this site

1

有可读性和可调试性之分。

我会将你的示例(顺便说一下,它不能编译)写成:

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

那样的话,如果我需要调试,我可以在返回语句上设置断点并查看retVal的值。此外,通常最好避免过于复杂的表达式,使用中间变量。首先,为了更容易地进行调试,其次,为了更容易阅读。

函数返回后,您可以检查retval。在gdb中,只需使用 finish 命令,就会立即得到带有结果的行。在我看来,您并没有获得太多收益。 - too honest for this site

0

我反对这种风格,尽管我知道许多人为了调试目的而这样做。

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

如果你觉得你想这样做,我非常非常建议你做两件事:

  • 使用C99,这样你就可以晚声明变量。
  • 使用const

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

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

请注意,const不能位于星号的左侧(const FILE *retVal = ...) ,因为这会使类型变成const FILE *。我们想要的是一个常量指针,而不是指向某个常量的指针。 const的目的是告诉人类读者:“我在这里命名这个值,但这不是我要搞乱的状态”。

1
const T * vT const * v相同的,你所需要的是 T * const v。同时注意许多编程风格只允许在块的开头或者甚至只有在函数作用域中定义(当然只针对局部变量)。 - too honest for this site
@Olaf 哦,笨拙的打字错误。已经修复了。是的,但那些风格通常是古老的 C99 之前的东西,当然最好忽略或修复。 - unwind
仍然被一些程序员使用(不包括我)。 (打字错误?好吧...可以接受;-) - too honest for this site
哦,在C11中仍然允许这样做。 - too honest for this site

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变量并没有添加任何有意义的信息(变量和方法名称传达相同的信息),因此并没有真正帮助读者。

0

我同意代码应该易读。然而,我不同意第一个代码更易于阅读甚至维护。

  • 可读性:需要阅读和理解的代码更多。虽然对于这个例子来说可能不难,但对于更复杂的类型可能会更困难。
  • 可维护性:如果您更改返回类型,则还必须更改retval声明。

许多编码风格要求在块的开头定义变量。其中一些甚至只允许在函数级别开始定义。因此,您在函数声明附近声明变量,远离返回值。

即使允许这样做:您获得了什么?它也可能隐藏返回值上的强制转换,因为编译器也会报告错误的返回类型 - 如果您启用了大多数警告。如果认真对待,这将有助于提高代码质量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接