我在假期周末学习了一点 C,我开始研究用 C 编写的其他程序。最后我研究了 GNU Netcat,认为它会是一个很好的例子。
看到600线的main()
功能,我有点震惊。这是正常的吗?如果这是正常的,这是否被认为是良好的 C 编码实践?
我在假期周末学习了一点 C,我开始研究用 C 编写的其他程序。最后我研究了 GNU Netcat,认为它会是一个很好的例子。
看到600线的main()
功能,我有点震惊。这是正常的吗?如果这是正常的,这是否被认为是良好的 C 编码实践?
有一位美国总统(林肯?)被问到男人的腿应该有多长。“足够长,可以从他的身体伸到地面,”他说。
回到主题:
诸如“清洁代码”之类的书籍的作者宣传每个函数只做一件事(我在这里大大简化了),所以理论上你main()
应该调用一个初始化函数,然后调用另一个函数来协调应用程序的工作,仅此而已。
在实践中,许多程序员发现很多微小的函数很烦人。一个可能更有用的指标是,一个功能通常应该适合一个屏幕,如果只是为了更容易看到和思考。
如果一个程序很复杂,并且它的大部分功能都在main()
. 本质上,您应该努力实现可管理性、可理解性和可读性。通常没有充分的理由让 main() 变得很大。
我经常在某些类型的应用程序中发现 main() 有数百行初始化,然后是大约 20 行顶级循环。
我的习惯是在我需要调用它们两次之前不中断函数。这有时会导致我编写一个 300 行的函数,但是一旦我看到同一个块出现两次,我就打破了那个块。
至于main,初始化例程通常是一次,所以600行听起来不合理。
嘿,这太可怕了,但我见过更糟的。我见过大型的、数千行的 fortran 程序,根本没有子例程。
我相信答案是:它应该适合编辑器窗口,并且应该具有低圈复杂度。
如果一个主程序只是一系列函数调用或计算,那么我想它可以是必要的,并且它可以免除编辑器窗口的约束。即使那样,我也会有点惊讶,没有一种自然的方法可以提取有意义的离散方法。
但是,如果它是测试和分支以及return
ing 和break
ing 和continue
-ing,那么它需要被分解成单独的和单独测试的功能组件。
无论使用哪种语言,我都会尝试将子例程方法限制为大致在一页代码中可见的内容,并尽可能将功能提取到子例程中。
对于任何实现来说,600 行听起来都相当长。也许有一些压倒一切的原因。传递论点和清晰性(我没有看过你发布的例子),但它听起来是在通常实践的远端,应该可以细分这个任务。
我怀疑它是通过多年来不断增加的功能而开发的,并且没有人停止并对其进行重构以使其更具可读性/可维护性。如果没有针对此的单元测试(并且根据我的经验,main()
方法通常不会得到书面测试——无论出于何种原因),那么人们不愿意重构它是可以理解的。
希望他们正在计划重构。这看起来非常粗糙。
443 while (optind < argc) {
444 const char *get_argv = argv[optind++];
445 char *q, *parse = strdup(get_argv);
446 int port_lo = 0, port_hi = 65535;
447 nc_port_t port_tmp;
448
449 if (!(q = strchr(parse, '-'))) /* simple number? */
450 q = strchr(parse, ':'); /* try with the other separator */
451
452 if (!q) {
453 if (netcat_getport(&port_tmp, parse, 0))
454 netcat_ports_insert(old_flag, port_tmp.num, port_tmp.num);
455 else
456 goto got_err;
457 }
458 else { /* could be in the forms: N1-N2, -N2, N1- */
459 *q++ = 0;
460 if (*parse) {
461 if (netcat_getport(&port_tmp, parse, 0))
462 port_lo = port_tmp.num;
463 else
464 goto got_err;
465 }
466 if (*q) {
467 if (netcat_getport(&port_tmp, q, 0))
468 port_hi = port_tmp.num;
469 else
470 goto got_err;
471 }
472 if (!*parse && !*q) /* don't accept the form '-' */
473 goto got_err;
474
475 netcat_ports_insert(old_flag, port_lo, port_hi);
476 }
477
478 free(parse);
479 continue;
480
481 got_err:
482 free(parse);
483 ncprint(NCPRINT_ERROR, _("Invalid port specification: %s"), get_argv);
484 exit(EXIT_FAILURE);
485 }
600 线主干线是一个警告信号。但是如果你看它,除了这样做之外,看不到任何将它分解成更小部分的方法。
void the_first_part_of_main(args...);
void the_second_part_of_main(args...);
...
main()
{
the_first_part_of_main();
the_second_part_of_main();
...
}
那么你应该不管它。
按照某些标准,任何类型的 600 行函数都是一个坏主意,但没有理由将 main 与任何其他函数区别对待。
我能想到出现这种情况的唯一原因是程序开发得很快,随着它的发展,没有人会费心将它分成更多的逻辑单元。
我想说你的例程应该尽可能长/短,以便有效、可靠和自动测试。一个 600 条语句的例程可能有多个路径通过它,并且例程的组合可能会很快变得非常大。我尝试将功能分解为易于阅读的功能。功能要么是“功能性的”,要么是“叙述性的”。一直包括单元测试。
我个人的编码风格是尝试仅使用 main 函数进行命令行参数解析,以及程序需要的任何大额初始化。
我见过的几乎所有的 600 行函数也都是愚蠢的。不必如此。
然而,在这些情况下,程序员未能提供一些缩小的视图,并为部分赋予有意义的名称 - 高级(如 Initialize())和低级(需要常见的 3 -line 模式并将其隐藏在一个名称下,并带有参数)。
在极端愚蠢的情况下,他们会在不需要时优化函数调用性能。
尽可能短。通常,每当有一个操作我可以指定一个名称时,我都会为它创建一个新方法。
main()
,就像任何函数一样,应该与它需要的一样大。“正如它所需要的那样”会根据它需要做什么而有很大的不同。话虽如此,在大多数情况下,它不应该超过几百行。600 行有点大,其中一些可以/应该重构为单独的函数。
举一个极端的例子,我所在的一个团队的任务是加速一些代码以驱动 3D 显示。代码最初是由一个线头编写的,他显然是用老式的 FORTRAN 自学编程的;main()
超过五千行代码,#include
到处都是随机位。他没有将代码分解成函数,而是简单地分支到main()
via内的子例程goto
(在 13 到 15 个 goto 之间的某个地方,看似随机地分支两个方向)。作为第一步,我们简单地打开了 1 级优化;编译器迅速吞噬了所有可用的内存和交换空间并使内核恐慌。代码太脆弱以至于我们无法编写任何代码改变而不破坏某些东西。我们最终告诉客户他们有两个选择:让我们从头开始重写整个系统或购买更快的硬件。
他们购买了更快的硬件。