【问题标题】:Why are some functions extremely long? (ideas needed for an academic research!) [closed]为什么有些函数特别长? (学术研究所需的想法!)[关闭]
【发布时间】:2010-11-10 18:51:47
【问题描述】:

我正在写一个关于极长函数的小型学术研究项目。显然,我不是在寻找 examples for bad programming,而是在寻找有意义的 100、200 和 600 行长函数的示例。

我将使用为Master's degree written in the Hebrew University 编写的脚本研究 Linux 内核源代码,该脚本测量不同的参数,例如代码行数、function complexity(由 MCC 测量)和其他好东西。顺便说一句,这是一本关于代码分析的精辟研究,推荐阅读材料。

如果您能想出为什么任何函数应该特别长的任何充分理由,我很感兴趣?我将研究 C,但来自任何语言的示例和参数都会很有用。

【问题讨论】:

    标签: c function code-structure mcc


    【解决方案1】:

    我可能会因此而受到抨击,但可读性。可以分解为 N 个函数调用(在其他地方没有使用的函数)的高度串行但独立的执行并没有真正从分解中受益。除非您将满足函数长度的任意最大值视为一种好处。

    我宁愿滚动浏览 N 个函数大小的代码块,而不是浏览整个文件,点击 N 个函数。

    【讨论】:

    • 吹毛求疵 - 这是炮弹(在 AA 火的意义上),“炮弹”是公关人员。
    • 啊,没错。只要我不接住和放音我就没事。
    • 好处是如果块真的是独立的,那么你可以通过实际分解它们来证明它。您和任何维护者都不能将状态存储在持续函数长度的变量中,在许多不同的地方进行修改,并且只有非常复杂的不变量。一旦一个函数变长,一个大范围的局部变量就会有许多全局变量的讨厌的特征。通过将其拆分为函数,您无法修改您没有明显传递引用的任何内容。当然,如果分解成功,你已经证明你不需要打扰;-)
    • 我不同意你的观点,帕特罗斯。我有一个大约 1000 行长的函数。它处理了 3 个不同的处理阶段,这些阶段仅在一个函数中完成,但无法分辨每个阶段的开始和结束位置,或者了解整个流程。我把它分成了大约 10 个主要函数调用的函数,你瞧,我能够很好地了解流程,即使这些函数没有在其他任何地方调用。将其视为文本文档中的折叠标题。
    • @patros:当然,我并不是说如果您遵循某些特定约定,您会发现您的代码更容易处理。我是说其他人会,为了概述的优势,他们不必阅读多页代码来查看函数的串行/独立结构。就我个人而言,我对此事相当矛盾:除非风格指南另有规定,否则我编写的函数比某些人想要的要长得多。我不认为处理 5 或 6 个变量是个问题。
    【解决方案2】:

    switch 语句中有很多值?

    【讨论】:

    • 我也想说同样的话——我已经在控制器的状态机中看到了巨大的函数作为巨大的 switch 语句
    • 我在旧的 Windows 程序中看到了巨大的 switch 语句。问题是,这些程序是否需要这样编写?答案是否定的。
    • 巨大的 switch 语句有什么问题?很简单,即使很长。
    • 我在旧的 Linux 程序中看到了巨大的 switch 语句。以及新的 Linux 程序。我不认为这是一个仅限 Windows 的问题 ;-) 正如 David 所指出的,它也不总是一个问题。
    • 如果它不适合单个屏幕,则很难掌握。我曾经看到一个嵌套开关的函数是 16000 行。真是难以理解。
    【解决方案3】:

    从其他来源生成的任何东西,即来自解析器生成器或类似的有限状态机。如果它不是供人类消费的,那么美学或可维护性问题就无关紧要了。

    【讨论】:

      【解决方案4】:

      随着时间的推移,函数会变得更长,尤其是当它们被多组开发人员修改时。

      举个例子:我最近(大约 1 年或 2 年前)重构了一些 2001 年左右的遗留图像处理代码,其中包含几千行函数。不是几个几千行的文件 - 几个几千行的函数。

      多年来,在没有真正努力正确重构它们的情况下,向它们添加了如此多的功能。

      【讨论】:

      • 所以这本身就是一个糟糕的编程,恕我直言——也许是环境所迫。
      • 对,但问题是“为什么有些函数非常长?” - 这就是我的答案:-)
      【解决方案5】:

      阅读 McConnell 的 Code Complete 中关于子例程的章节,它提供了关于何时应该将事物分解为函数的指南和指针。如果您有一些算法不适用这些规则,那可能是使用长函数的一个很好的理由。

      【讨论】:

      • 你能想出一个具体的例子,我可以链接到吗?
      【解决方案6】:

      生成的代码可以生成非常长的函数。

      【讨论】:

        【解决方案7】:

        我最近编写的唯一代码是使它们更小并没有太大的作用,或者会使代码的可读性降低。超过一定长度的函数在某种程度上本质上是坏的概念只是盲目的教条。就像任何盲目应用的教条一样,它让追随者无需真正考虑在任何特定情况下适用什么......

        最近的例子...

        解析并验证具有简单名称=值结构的配置文件到数组中,将每个值转换为我找到的值,这是一个巨大的 switch 语句,每个配置选项一个案例。为什么?我本可以拆分为对 5/6 行琐碎函数的大量调用。这将为我的班级增加大约 20 个私人成员。它们都没有在其他任何地方重复使用。将它分解成更小的块并没有增加足够的价值,因此从原型开始就一直如此。如果我想要另一个选项,请添加另一个案例。

        另一种情况是同一应用程序中的客户端和服务器通信代码,以及它的客户端。很多读/写调用都可能失败,在这种情况下,我保释并返回 false。所以这个函数基本上是线性的,几乎每次调用后都有保释点(如果失败,返回)。同样,将其变小并没有任何好处,也无法真正将其变小。

        我还应该补充一点,我的大部分功能都是几个“全屏”,我在更复杂的领域努力将其保持为一个“全屏”,仅仅是因为我可以一次查看整个功能。对于本质上基本上是线性的函数并且没有很多复杂的循环或条件进行,因此流程很简单,这是可以的。 作为最后一点,我更喜欢在决定重构哪些代码时应用成本效益推理,并相应地确定优先级。有助于避免永远半途而废的项目。

        【讨论】:

          【解决方案8】:

          有时我发现自己正在编写一个平面文件(供第三方使用),其中包含所有链接的标题、预告片和详细记录。为了计算摘要而使用长函数比设计一些方案通过许多小函数来回传递值更容易。

          【讨论】:

            【解决方案9】:

            我认为重要的一点是,不同的语言和工具具有与函数相关的不同词汇范围。

            例如,Java 允许您使用注释来抑制警告。可能需要限制注释的范围,因此您可以为此目的保持函数的简短。在另一种语言中,将该部分拆分为它自己的函数可能是完全任意的。

            有争议:在 JavaScript 中,我倾向于只创建函数以重用代码。如果 sn-p 只在一个地方执行,我发现在函数引用的意大利面条之后跳转文件是很麻烦的。我认为闭包有助于并因此加强了更长的 [父] 功能。由于 JS 是一种解释性语言,并且实际代码是通过网络发送的,因此最好保持代码的长度较短——创建匹配的声明和引用并没有帮助(这可能被认为是过早的优化)。一个函数必须在 JS 中变得相当长,然后我才决定为了“保持函数简短”的明确目的而将其拆分。

            同样在 JS 中,有时整个“类”在技术上是一个包含许多封闭子函数的函数,但有一些工具可以帮助处理它。

            另一方面,在 JS 中,变量具有函数长度的范围,因此这是可能限制给定函数长度的一个因素。

            【讨论】:

              【解决方案10】:

              我遇到的很长的函数不是用 C 编写的,所以你必须决定这是否适用于你的研究。我想到的是一些长达数百行的 PowerBuilder 函数,原因如下:

              • 它们是 10 多年前编写的,当时人们还没有考虑到编码标准。
              • 开发环境使得创建函数有点困难。这不是一个好的借口,但这是有时会阻碍你正常工作的小事之一,我猜有人只是懒惰了。
              • 随着时间的推移,功能不断发展,增加了代码和复杂性。
              • 函数包含巨大的循环,每次迭代可能以不同的方式处理不同类型的数据。使用数十个(!)局部变量、一些成员变量和一些全局变量,它们变得极其复杂。
              • 又旧又丑,没有人敢将它们重构为更小的部分。处理了这么多特殊情况,拆开就是自找麻烦。

              这是另一个明显的不良编程实践与现实相遇的地方。虽然任何一年级的 CS 学生都可以说这些野兽很糟糕,但没有人会花钱让它们看起来更漂亮(至少目前,它们仍然可以做到)。

              【讨论】:

                【解决方案11】:

                到目前为止,我看到/写的最常见的是长 switch 语句或 if/else 半开关语句,用于该语言的 switch 语句中不能使用的类型(已经提到过几次)。生成的代码是一个有趣的案例,但我在这里关注的是人工编写的代码。看看我当前的项目,上面没有包含的唯一真正长的函数(296 LOC/650 LOT)是一些牛仔代码,我用作早期评估我计划在未来使用的代码生成器的输出。我肯定会对其进行重构,从而将其从列表中删除。

                许多年前,我正在开发一些具有长期功能的科学计算软件。该方法使用了大量的局部变量,并且不断重构该方法,从而导致每次分析都会产生可测量的差异。即使在这部分代码中 1% 的改进也节省了数小时的计算时间,因此函数保持了很长时间。从那以后我学到了很多东西,所以我不能说我今天会如何处理这种情况。

                【讨论】:

                  【解决方案12】:

                  速度:

                  • 调用函数意味着压入堆栈,然后跳转,然后再次存储在堆栈中,然后再次跳转。如果你对函数使用参数,你通常会有更多的推送。

                  考虑一个循环:

                  for...
                     func1
                  

                  在循环中,所有这些推动和跳跃都可能是一个因素。

                  这在很大程度上通过在C99 上的Inline Functions 的呈现得到了解决,并且在此之前是非正式的,但是之前编写的一些代码,或者是出于兼容性考虑而创建的,可能因为这个原因已经很长了。

                  Inline 也有它的流程,一些在Inline Functions link 上进行了描述。

                  编辑:

                  作为一个函数调用如何使程序变慢的例子:

                  4         static void
                  5 do_printf()
                  6 {
                  7         printf("hi");
                  8 }
                  9         int
                  10 main()
                  11 {
                  12         int i=0;
                  13         for(i=0;i<1000;++i)
                  14                 do_printf();
                  15 }
                  

                  这会产生(GCC 4.2.4):

                   .
                   . 
                   jmp    .L4
                   .L5:
                  call    do_printf
                  addl    $1, -8(%ebp)
                   .L4:
                  cmpl    $999, -8(%ebp)
                  jle .L5
                  
                   .
                   .
                  do_printf:
                  pushl   %ebp
                  movl    %esp, %ebp
                  subl    $8, %esp
                  movl    $.LC0, (%esp)
                  call    printf
                  leave
                  ret
                  

                  反对:

                           int
                   main()
                   {
                           int i=0;
                           for(i=0;i<1000;++i)
                                   printf("hi");
                   }
                  

                  或反对:

                   4         static inline void __attribute__((always_inline)) //This is GCC specific!
                   5 do_printf()
                   6 {
                   7         printf("hi");
                   8 }
                  

                  两者都产生(GCC 4.2.4):

                  jmp .L2
                  .L3:
                  movl    $.LC0, (%esp)
                  call    printf
                  addl    $1, -8(%ebp)
                  .L2:
                  cmpl    $999, -8(%ebp)
                  jle .L3
                  

                  哪个更快。

                  【讨论】:

                  • 感谢和 Toda Raba。我认为内联函数的主要问题是它们的参数列表,在许多情况下(没有双关语)。
                  【解决方案13】:

                  XML 解析代码通常在一个设置函数中进行大量转义字符处理。

                  【讨论】:

                    【解决方案14】:

                    我处理(不是写)的函数变得很长,因为被扩展和扩展,没有人花时间去重构函数。他们只是不断地向功能添加逻辑,而不考虑全局。

                    我处理了大量的剪切-粘贴开发...

                    因此,对于本文而言,需要关注的一个方面是维护计划/周期不佳等。

                    【讨论】:

                      【解决方案15】:

                      一些尚未明确提及的想法:

                      • 重复性任务,例如该函数读取一个包含 190 列的数据库表,并且必须将它们作为平面文件输出(假设需要单独处理列,因此无法对所有列进行简单循环)。当然,您可以创建 19 个函数,每个函数输出 10 列,但这不会让程序变得更好。
                      • 复杂、冗长的 API,例如 Oracle's OCI。当看似简单的操作需要大量代码时,很难将其分解为有意义的小函数。

                      【讨论】:

                        猜你喜欢
                        • 2023-03-08
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2012-04-05
                        • 1970-01-01
                        • 1970-01-01
                        • 2011-02-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多