关于C#:为什么某些功能非常长?

Why are some functions extremely long? (ideas needed for an academic research!)

我正在写一个关于超长函数的小型学术研究项目。显然,我不是在寻找坏编程的例子,而是100、200和600行长的函数的例子,这是有意义的。

我将使用一个为希伯来大学硕士学位编写的脚本来研究Linux内核源代码,该脚本测量不同的参数,如代码行数、函数复杂性(由mcc测量)和其他优点。顺便说一下,这是一个关于代码分析的整洁的研究,也是一个推荐的阅读材料。

我很感兴趣,如果你能想出一个很好的理由,为什么任何函数都应该非常长?我将研究C,但是任何语言的例子和论据都是很有用的。


我可能会因此受到指责,但可读性。一个高度串行但独立的执行,可以分解成n个函数调用(在其他地方不使用的函数),这并不能真正从分解中受益。除非您将满足函数长度的任意最大值作为一项好处。

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


switch语句中有很多值?


从其他来源生成的任何东西,例如从解析器生成器或类似工具生成的有限状态机。如果它不是为人类消费而设计的,那么美学或可维护性方面的考虑是无关紧要的。


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

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

在过去的几年中,他们添加了这么多的功能,而没有真正投入到重构它们的工作中。


阅读麦康奈尔代码中关于子程序的完整章节,它有指导方针和指针,告诉你什么时候应该把事情分解成函数。如果您有一些不适用这些规则的算法,这可能是拥有长函数的一个很好的理由。


我最近编写的代码中,只有一个没有太大作用,使它们变小,或者使代码的可读性变差。一个超过一定长度的函数在某种程度上本质上是不好的,这个概念仅仅是盲目的教条。就像任何盲目应用的教条一样,它减轻了追随者实际思考在任何特定情况下应用什么的必要性……

最近的示例…

解析一个简单的name=value结构的配置文件,并将其验证为一个数组,在找到它时转换每个值,这是一个巨大的switch语句,每个config选项一个案例。为什么?我可以分成许多对5/6行琐碎函数的调用。这会给我的班增加大约20个私人成员。它们都不能在其他地方重用。把它分解成更小的块并没有增加足够的价值,所以从原型开始它就一直是一样的。如果我需要另一个选项,请添加另一个案例。

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

我还应该补充一下,我的大部分功能都是几个"屏幕提示",我努力在更复杂的领域将其保持为一个"屏幕提示",因为我可以一次查看整个功能。对于本质上基本上是线性的函数,没有很多复杂的循环或条件,所以流程很简单。最后一点,在决定要重构的代码时,我更喜欢应用成本效益推理,并相应地确定优先级。有助于避免永久性的半成品项目。


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


速度:

  • 调用函数意味着推到堆栈,然后跳,然后再次存储到堆栈,然后再次跳。如果使用函数的参数,通常会有多个推送。

考虑一个循环:

1
2
for...
   func1

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

这在很大程度上是通过在c99上展示内联函数来解决的,而且在这之前是非正式的,但是出于这个原因,以前编写的或基于兼容性创建的一些代码可能已经很长时间了。

此外,inline还有它的流,一些在inline函数链接中进行了描述。

编辑:

例如,调用函数可以使程序变慢:

1
2
3
4
5
6
7
8
9
10
11
12
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 .
 .
 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

反对:

1
2
3
4
5
6
7
         int
 main()
 {
         int i=0;
         for(i=0;i<1000;++i)
                 printf("hi");
 }

或反对:

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

两种产品(GCC 4.2.4):

1
2
3
4
5
6
7
8
jmp .L2
.L3:
movl    $.LC0, (%esp)
call    printf
addl    $1, -8(%ebp)
.L2:
cmpl    $999, -8(%ebp)
jle .L3

哪个更快。


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

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


我遇到的非常长的函数不是用C编写的,所以您必须决定这是否适用于您的研究。我想到的是一些PowerBuilder函数,它们有几百行长,原因如下:

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

这是另一个明显的不良编程实践与现实相符合的地方。尽管任何一个一年级的CS学生都会说这些野兽是坏的,但没有人会花任何钱让它们看起来更漂亮(至少目前为止,它们仍然能做到)。


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

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

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

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

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


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


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

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

我处理的函数(不是写的)变长了,因为它们被扩展和扩展,没有人花费时间来重新考虑函数。他们只是不断地向函数添加逻辑,而不考虑全局。

我处理了很多剪切粘贴的开发…

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


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