我正在写一个关于极长函数的小型学术研究项目。显然,我不是在寻找糟糕编程的示例,而是寻找有意义的 100、200 和 600 行长函数的示例。
我将使用为希伯来大学的硕士学位编写的脚本来研究 Linux 内核源代码,该脚本测量不同的参数,如代码行数、函数复杂度(由 MCC 测量)和其他好东西。顺便说一句,这是一本关于代码分析的精巧研究,也是推荐阅读材料。
如果您能想到任何函数应该特别长的任何充分理由,我很感兴趣?我将研究 C,但任何语言的示例和参数都会很有用。
我正在写一个关于极长函数的小型学术研究项目。显然,我不是在寻找糟糕编程的示例,而是寻找有意义的 100、200 和 600 行长函数的示例。
我将使用为希伯来大学的硕士学位编写的脚本来研究 Linux 内核源代码,该脚本测量不同的参数,如代码行数、函数复杂度(由 MCC 测量)和其他好东西。顺便说一句,这是一本关于代码分析的精巧研究,也是推荐阅读材料。
如果您能想到任何函数应该特别长的任何充分理由,我很感兴趣?我将研究 C,但任何语言的示例和参数都会很有用。
我可能会因此而受到抨击,但可读性。可以分解为 N 个函数调用(在其他地方没有使用的函数)的高度串行但独立的执行并不能真正从分解中受益。除非您将满足函数长度的任意最大值视为一种好处。
我宁愿滚动浏览 N 个函数大小的代码块,也不愿浏览整个文件,点击 N 个函数。
switch 语句中有很多值?
从其他来源生成的任何东西,即来自解析器生成器或类似的有限状态机。如果它不是供人类消费的,那么美学或可维护性问题就无关紧要了。
随着时间的推移,函数会变得更长,尤其是当它们被许多开发人员修改时。
举个例子:我最近(大约 1 年或 2 年前)重构了一些 2001 年左右的遗留图像处理代码,其中包含几千行函数。不是几个几千行的文件 - 几个几千行的功能。
多年来,在没有真正努力正确重构它们的情况下,向它们添加了如此多的功能。
阅读 McConnell 的 Code Complete 中关于子例程的章节,它有指导方针和何时应该将事物分解为函数的指针。如果您有一些不适用这些规则的算法,那可能是使用长函数的一个很好的理由。
生成的代码可以生成非常非常长的函数。
我最近编写的唯一代码是使它们更小或使代码的可读性降低的地方并没有太大的作用。超过一定长度的函数在某种程度上本质上是坏的概念只是盲目的教条。就像任何盲目应用的教条一样,它使追随者无需真正考虑在任何特定情况下适用什么……
最近的例子...
解析并验证具有简单名称=值结构的配置文件到数组中,将每个值转换为我找到的值,这是一个巨大的 switch 语句,每个配置选项一个案例。为什么?我本可以拆分为对 5/6 行琐碎函数的大量调用。这将为我的班级增加大约 20 个私人成员。它们都没有在其他任何地方重复使用。把它分解成更小的块并没有增加足够的价值,所以从原型开始就一直如此。如果我想要另一个选项,请添加另一个案例。
另一种情况是同一应用程序中的客户端和服务器通信代码及其客户端。很多读/写调用都可能失败,在这种情况下,我保释并返回 false。所以这个函数基本上是线性的,几乎每次调用后都有保释点(如果失败,返回)。同样,将其变小也无济于事,也无法真正使其变小。
我还应该补充一点,我的大部分功能都是几个“全屏”,我在更复杂的领域努力将其保持为一个“全屏”,仅仅是因为我可以一次查看整个功能。对于本质上基本上是线性的函数并且没有很多复杂的循环或条件进行,因此流程很简单,这是可以的。作为最后一点,我更喜欢在决定重构哪些代码时应用成本效益推理,并相应地确定优先级。有助于避免永远半途而废的项目。
有时我发现自己正在编写一个平面文件(供第三方使用),其中包含所有链接的标题、预告片和详细记录。为了计算摘要而使用长函数比设计一些方案通过许多小函数来回传递值更容易。
我认为重要的一点是,不同的语言和工具具有与函数相关的不同词汇范围。
例如,Java 允许您使用注释来抑制警告。可能需要限制注释的范围,因此您为此目的保持函数简短。在另一种语言中,将该部分分解为它自己的功能可能是完全任意的。
争议:在 JavaScript 中,我倾向于只创建函数以重用代码。如果一个片段只在一个地方执行,我发现在函数引用的意大利面之后跳转文件是很麻烦的。我认为闭包有助于并因此加强了更长的 [父] 功能。由于 JS 是一种解释性语言,并且实际代码是通过网络发送的,因此最好保持代码的长度较短——创建匹配的声明和引用并没有帮助(这可能被认为是过早的优化)。在我决定为了“保持函数简短”的明确目的而决定将其分割之前,一个函数必须在 JS 中变得相当长。
同样在 JS 中,有时整个“类”在技术上是一个包含许多封闭子函数的函数,但有一些工具可以帮助处理它。
另一方面,在 JS 中,变量具有函数长度的范围,因此这是可能限制给定函数长度的一个因素。
我遇到的很长的函数不是用 C 编写的,所以你必须决定这是否适用于你的研究。我想到的是一些长达数百行的 PowerBuilder 函数,原因如下:
这是另一个明显的不良编程实践与现实相遇的地方。虽然任何一年级的 CS 学生都可以说这些野兽很糟糕,但没有人会花任何钱让它们看起来更漂亮(至少目前,它们仍然可以提供)。
到目前为止,我看到/写的最常见的是长 switch 语句或 if/else 半开关语句,用于该语言的 switch 语句中不能使用的类型(已经提到过几次)。生成的代码是一个有趣的案例,但我在这里关注的是人工编写的代码。看看我目前的项目,上面没有包含的唯一真正长的函数(296 LOC/650 LOT)是一些牛仔代码,我用作早期评估我计划在未来使用的代码生成器的输出。我肯定会重构它,从这个列表中删除它。
许多年前,我正在开发一些具有长期功能的科学计算软件。该方法使用了大量的局部变量,并且不断重构该方法,从而导致每次分析都会产生可测量的差异。即使在这部分代码中 1% 的改进也节省了数小时的计算时间,因此函数保持了很长时间。从那以后我学到了很多东西,所以我无法谈论我将如何处理今天的情况。
速度:
考虑一个循环:
for...
func1
在循环中,所有这些推动和跳跃都可能是一个因素。
这在很大程度上通过C99上的内联函数的呈现得到了解决,并且在此之前是非官方的,但是之前编写的一些代码,或者是在考虑到兼容性的情况下创建的,可能因为这个原因已经很长时间了。
Inline 也有它的流程,一些在Inline Functions 链接上进行了描述。
编辑:
作为一个函数调用如何使程序变慢的示例:
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 解析代码通常在一个设置函数中进行大量转义字符处理。
我处理(不是写)的函数变得很长,因为被扩展和扩展,没有人花时间重构函数。他们只是不断地向功能添加逻辑,而不考虑全局。
我处理了很多剪切-粘贴开发...
因此,对于本文而言,要考虑的一个方面是维护计划/周期不佳等。
一些尚未明确提及的想法: