为什么有些function非常长? (学术研究需要的想法!)

我正在写一个关于极长function的小型学术研究项目。 显然,我不是在寻找错误编程的例子,而是寻找100,200和600行长函数的例子。

我将使用在希伯来大学编写的硕士学位编写的脚本来调查Linux内核源代码,该脚本测量不同的参数,如代码行数, 函数复杂度 (由MCC测量)和其他好东西。 顺便说一下,这是一个关于代码分析和推荐阅读材料的简洁研究。

我很感兴趣,如果你能想出为什么任何函数应该特别长的任何理由? 我将研究C,但任何语言的例子和论据都会有很大用处。

我可能会因此而感到害怕,但可读性。 一个高度串行但独立的执行可以分解为N个函数调用(其他地方使用的函数)并没有真正受益于分解。 除非你计算在函数长度上满足任意最大值作为一个好处。

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

switch语句中有很多值?

从其他源生成的任何内容,即来自解析器生成器或类似的有限状态机。 如果它不是用于人类消费,则美学或可维护性问题是无关紧要的。

随着时间的推移,函数会变得更长,特别是如果它们被许多开发人员修改。

例证:我最近(~1年或2年前)重构了2001年左右的一些传统图像处理代码,其中包含几个几千行function。 不是几千行文件 – 几个几千行function。

多年来,他们添加了如此多的function,而没有真正努力正确地重构它们。

阅读关于子程序的McConnell代码完成章节,它有关于何时应该将function分解为函数的指南和指针。 如果你有一些不适用这些规则的算法,那么这可能是拥有长函数的一个很好的理由。

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

我最近编写的唯一内容是它们在使它们变小或者使代码可读性降低方面没有太大成效。 超过一定长度的函数在某种程度上本质上是坏的概念只是盲目的教条。 就像任何盲目应用的教条一样,在任何特定情况下都可以让追随者真正想到适用的内容……

最近的例子……

解析并validation具有简单name = value结构的配置文件到一个数组中,在找到它时转换每个值,这是一个大规模的switch语句,每个配置选项一个case。 为什么? 我本来可以分成许多对5/6行琐碎函数的调用。 这将为我class增加约20名私人会员。 它们都不会在其他任何地方重复使用。 将它分解成较小的块只是没有增加足够的价值值得它,所以它自原型以来一直如此。 如果我想要另一个选项,请添加另一个案例。

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

我还应该补充一点,我的大多数function都是一些“屏幕”,我在更复杂的领域努力使它保持一个“屏幕”,只是因为我可以立即查看整个function。 对于本质上基本上是线性的并且没有大量复杂循环或条件的函数来说,这是好的,所以流程很简单。 作为最后一点,我更倾向于在决定重构哪些代码时应用成本效益推理,并相应地确定优先级。 有助于避免永久性的半成品项目。

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

我认为有一点意味着不同的语言和工具具有与function相关的不同词法范围。

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

有争议:在JavaScript中,我倾向于仅为了重用代码而创建函数。 如果一个片段只在一个地方执行,我发现在函数引用的意大利面后跳转文件是很麻烦的。 我认为闭包有助于加强[父]function。 由于JS是一种解释型语言,并且实际代码是通过线路发送的,因此保持代码长度很小是很好的 – 创建匹配的声明和引用没有帮助(这可以被认为是过早的优化)。 在我决定将其删除以达到“保持function简短”的明确目的之前,函数必须在JS中使用相当长的时间。

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

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

我遇到的很长时间的function都不是用C语言写的,所以你必须决定这是否适用于你的研究。 我想到的是一些PowerBuilderfunction,这些function有几百行,原因如下:

  • 它们是在十多年前由当时没有编码标准的人写的。
  • 开发环境使创建函数变得更加困难。 几乎没有一个好借口,但这是有时会阻碍你正常工作的小事之一,我想有人只是懒得。
  • 这些function随着时间的推移不断发展,增加了代码和复杂性。
  • 这些函数包含巨大的循环,每次迭代都可能以不同的方式处理不同类型的数据。 使用十(!)个局部变量,一些成员变量和一些全局变量,它们变得极其复杂。
  • 那么古老而丑陋,没有人敢把它们重构成更小的部分。 在他们处理了这么多特殊情况,将他们分开就是在寻找麻烦。

这是另一个明显糟糕的编程实践符合现实的地方。 虽然任何一年级的CS学生都可以说这些野兽很糟糕,但没有人愿意花钱让它们看起来更漂亮(考虑到至少现在,它们还能提供)。

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

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

速度:

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

考虑一个循环:

for... func1 

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

这在很大程度上是通过C99上的内联函数的呈现以及之前的非正式函数来解决的,但之前编写的一些代码,或者考虑到兼容性而创建的代码可能已经很长时间了。

Inline也有它的流程,有些在内联函数链接中有描述。

编辑:

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

 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 

哪个更快。

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

我处理的函数(不是写入)变得很长,因为它们被扩展和扩展,没有人花时间重新考虑函数。 他们只是不断添加逻辑function而不考虑大局。

我处理了很多cut-n-paste开发……

因此,对于论文来说,要看的一个方面是糟糕的维护计划/周期等。

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

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