条件的条件中的赋值是不好的做法吗?

假设我想编写一个在C中连接两个字符串的函数。 我写它的方式是:

void concat(char s[], char t[]){
    int i = 0;
    int j = 0;

    while (s[i] != '\0'){
        i++;
    }

    while (t[j] != '\0'){
        s[i] = t[j];
        i++;
        j++;
    }

    s[i] = '\0';

}

However, K&R in their book implemented it differently, particularly including as much in the condition part of the while loop as possible:

void concat(char s[], char t[]){
    int i, j;
    i = j = 0;
    while (s[i] != '\0') i++;

    while ((s[i++]=t[j++]) != '\0');

}

Which way is preferred? Is it encouraged or discouraged to write code the way K&R do? I believe my version would be easier to read by other people.

33
@corsiKa:在Swift中,i ++已经不复存在了,而且作业不再产生价值了,所以差不多40年后我们就拥有了这种风格绝对不再适用的语言。
额外 作者 gnasher729,
请注意,这两个代码块不相同。第一个代码块不会从 t 复制终止'\ 0',而首先退出)。这将使得到的 s 字符串没有终止'\ 0'(除非内存位置已经归零)。第二个代码块将在退出 while 循环之前复制终止'\ 0'
额外 作者 Redzarf,
对于亲密的选民:虽然我很难承认,但这个问题主要不是基于意见的。有客观的理由偏爱一个,而这不是一些深奥的,人为的代码。确实存在很多这两种类型的代码,一种明显优于另一种。
额外 作者 user22815,
我很震惊他们有'\ 0'的索引和比较,而不是像那样的东西(* s ++ = * t ++); (我的C非常生疏,我需要那些用于运算符优先级的parens ?)K&R是否发布了他们图书的新版本?他们的原着书有非常简洁和惯用的代码。
额外 作者 MrG,
@ user949300完整的ack,我也偶然发现了这个。特别是那些嘈杂的!='\ 0'比较。这不是经典的C风格,也不包含任何布尔禅。另一方面,它也不使用yoda条件逆转。混淆这段代码非常......
额外 作者 cmaster,
不要忘记,K&R于1978年首次发布。从那时起,我们的代码编码发生了一些小变化。
额外 作者 corsiKa,
可读性是一个非常私人的事情 - 即使在电传类型的时代。不同的人喜欢不同的风格。很多指令填充都与代码生成有关。在那些日子里,一些指令集(例如Data General)可以将几个操作塞入一个指令中。此外,在80年代早期,有一个神话,即使用括号生成更多指令。我必须生成汇编程序,以向代码审查员证明这是一个神话。
额外 作者 cup,
在远程打印机和面向行的编辑器时代,可读性非常不同。将所有这些东西压缩到一行上曾经更多可读。
额外 作者 user2357112,
就个人而言,我发现两者都很合理,而第二个例子对我来说,突出了这些价值观相互依赖的依赖性。我可能会在这两个例子之间写一些东西。令我困扰的是这两个这些例子虽然缺乏阵列安全性。如果 s [] 已经满了怎么办?
额外 作者 Nuzzolilo,

8 答案

总是喜欢清晰而不是聪明。在过去,最好的程序员是没有人能理解的代码。他们说,“我无法理解他的代码,他必须是一个天才”。如今最好的程序员是任何人都可以理解的代码。计算机时间现在比程序员的时间便宜。

任何傻瓜都可以编写计算机可以理解的代码。好   程序员编写人类可以理解的代码。 (M.福勒)

所以,没有doudbt,我会去选项A.这是我的最终答案。

79
额外
@ LukeA.Leber什么?一个IDE?请 ! explainxkcd.com/wiki/images/0/0f/real_programmers.png
额外 作者 nickfox,
@DanielJour Lol,在写完评论后几个小时,我想到了那个确切的反例,哈哈。这个是好的,原因有几个:1- C不提供任何类型的等效foreach结构,2-你只是在这里分配循环变量,就像你在for循环中一样。没有什么比写在数组内部更好的了。
额外 作者 nickfox,
@MilesRout有。任何具有副作用的代码都有问题,例如传递函数参数或评估条件。甚至没有提到 if(a = b)很容易被误认为 if(a == b)
额外 作者 nickfox,
@Luke:“我的IDE可以清理X,因此它不是问题”是相当令人难以置信的。如果这不是问题,为什么IDE会让它变得如此容易“修复”?
额外 作者 Piers C,
-1对于计算机时间现在比程序员时间便宜,这是导致计算机在每次主要软件更新后变慢的心态。
额外 作者 Malcolm,
@DanielJour就个人而言,无论如何我都不会在一行中写这个,分配和检查在循环中都是一个单独的行。编写一些额外的行来避免在条件中进行一次分配并不困扰我,如果不止一次,我会将它提取到一个函数中。
额外 作者 MD-Tech,
@hvd那么break语句有什么问题?
额外 作者 MD-Tech,
我很想知道这个概念是否最终会破坏高尔夫码
额外 作者 pr1268,
@JopV。尽管如此,这是事实。
额外 作者 Tulains Córdova,
@ArthurHavlicek - 任何体面的IDE都会标记这样的任务,从而明确突出可疑区域,使其不太可能陷入困境。伟大的IDE甚至有一键式重构机制。
额外 作者 ghellquist,
@Kevin - 现代IDE标记出存在潜在问题的情况,因为存在潜在问题。这可能(例如)因为缺少括号( if(a = someFunction()== true))。正确括起表达式后,不会显示任何警告。这消除了关于这种表达式中的评估顺序的所有可能的混淆。
额外 作者 ghellquist,
+1“考虑到调试的难度是首先编写程序的两倍,如果你在编写程序时那么聪明,你将如何调试它?” B.W.Kernighan
额外 作者 Christophe,
非常精辟的意识形态,但事实仍然是条件中的任务没有错。最好是从循环中提前退出或在循环之前和之内复制代码。
额外 作者 Miles Rout,
@Malcolm就我而言,并不多,但是,在循环条件下,我也没有看到太多关于赋值的问题。就其他一些人而言,它是一个伪装的 goto ,并且更难以推断代码的正确性。我认为这是一个有效的观点,即使它是我不同意的观点。
额外 作者 hvd,
@Malcolm在循环条件之外编写它的唯一方法是代码重复, break; 语句或单独的函数。将它提取到函数中应该没问题,但其他两个通常不被认为是良好的实践,充其量它们被认为比循环条件中的赋值更糟糕。
额外 作者 hvd,
@ArthurHavlicek我同意你的一般观点,但条件中带副作用的代码真的不是那么罕见: while((c = fgetc(file))!= EOF)作为第一个来到我的心神。
额外 作者 Ben,

与TulainsCórdova的答案相同的黄金法则是确保编写可理解的代码。但我不同意这个结论。这条黄金法则意味着编写最终维护代码的典型程序员可以理解的代码。并且是判断最终维护代码的典型程序员的最佳判断。

对于没有以C开头的程序员,第一个版本可能更容易理解,原因你已经知道了。

对于那些用C风格长大的人来说,第二个版本可能更容易理解:对于他们来说,代码对代码的作用同样可以理解,对于他们来说,它留下的问题更少,为什么它的编写方式和它们的编写方式,垂直空间越小意味着可以在屏幕上显示更多上下文。

你必须依靠自己的良好感觉。您希望哪些受众最容易理解您的代码?这个代码是为公司编写的吗?然后目标受众可能是该公司的其他程序员。这是一个个人爱好项目,除了你自己,没有人可以工作吗?那么你就是你自己的目标受众。您要与其他人分享此代码吗?那些其他人就是你的目标受众。选择与该受众群体匹配的版本。不幸的是,没有一种首选方式可以鼓励。

32
额外

EDIT: The line s[i] = '\0'; was added to the first version, thereby fixing it as described in variant 1 below, so this doesn't apply to the current version of the question's code anymore.

第二个版本具有正确的独特优势,而第一个版本没有 - 它没有正确地终止目标字符串。

“条件赋值”允许表达“在检查空字符之前复制每个字符”的概念,非常简洁,并且使编译器的优化更容易,尽管现在很多软件工程师发现这种代码风格不太可读。如果你坚持使用第一个版本,你必须要么

  1. 在第二个循环结束后添加空终止(添加更多代码,但你可以说可读性值得)或者
  2. 将循环体更改为“首先分配,然后检查或保存指定的字符,然后增加索引”。检查循环中间的条件意味着打破循环(降低清晰度,大多数纯粹主义者不赞成)。保存指定的字符将意味着引入临时变量(降低清晰度和效率)。在我看来,这两种方法都会消灭这种优势。
14
额外
正确比可读和简洁更好。
额外 作者 MrG,

TulainsCórdova和hvd的答案很好地涵盖了清晰度/可读性方面。让我投入确定范围作为支持条件分配的另一个原因。在条件中声明的变量仅在该语句的作用域中可用。事后您不能使用该变量。 for 循环已经使用了很长时间。而且即将到来的C ++ 17 如果切换 引入了类似的语法:

if (int foo = bar(); foo > 42) {
    do_stuff();
}

foo = 23;  //compiler error: foo is not in scope
6
额外

In the days of K&R

  • ‘C’ was portable assembly code
  • It was used by programmers that thought in assembly code
  • The compiler did not do much optimization
  • Most computers had “complex instruction sets”, for example while ((s[i++]=t[j++]) != '\0') would map to one instruction on most CPUs (I expect the Dec VAC)

有几天

  • 大多数阅读C代码的人都不是汇编代码程序员
  • C编译器进行了大量优化,因此更简单的读取代码很可能被转换为相同的机器代码。

(关于总是使用大括号的注释 - 由于有一些“不需要的” {} ,第一组代码会占用更多空间,根据我的经验,这些代码经常会阻止与编译器严重合并的代码并允许错误的“;”展示位置可供工具检测。)

但是在过去,代码的第二版本已经阅读过了。 (如果我做对了!)

concat(char* s, char *t){      
    while (*s++);
    --s;
    while (*s++=*t++);
}
3
额外

不,这是非常标准和正常的C风格。你的例子很糟糕,因为它应该只是一个for循环,但总的来说没有任何问题

if ((a = f()) != NULL)
    ...

例如(或与while)。

3
额外
@jcast:这个开发人员对指针是真还是假,或者整数是真还是假非常不舒服,因为指针不是真或假,而且整数不是真或假。
额外 作者 gnasher729,
有人看到 if(a = f())... 可能会认为它可能被错误地输入了那些意味着 if(a == f())... 代码 if((a = f())!= 0)... ,但是是等价的,但有人看到它不太可能认为程序员的意思是 if((a) == f())!= 0)......
额外 作者 supercat,
我认为OP使用C是因为它是他选择的语言,但无论他在他的例子中使用的确切语法是否可以100%在另一种语言中复制,这个问题实际上与语言无关。
额外 作者 Tulains Córdova,
这有点不对劲; `!= NULL`和它在C条件中的亲属是natter,只是为了安抚那些对值为真或假的概念不满意的开发人员(反之亦然)。
额外 作者 John Washburn,
不,更明确地说(x!= NULL)!= 0 。毕竟,那就是C 真的检查,对吗?
额外 作者 John Washburn,
“检查某些东西是否不等于假是不是你用任何语言写条件的方式。”究竟。
额外 作者 John Washburn,
@jcast不,不是。检查某些内容是否不等于false并不是您如何使用任何语言编写条件。
额外 作者 Miles Rout,
@jcast不,包含!= NULL 更明确。
额外 作者 Miles Rout,
@TulainsCórdova我同意你的观点,但我仍然认为我的答案是相关的。我不认为我的答案是一个真正正确的答案,但不应该被接受。
额外 作者 Miles Rout,
@ gnasher729 false和true只是0而且不是0。
额外 作者 Miles Rout,
根据定义,NULL将在上下文中提升为“0”,因此@Mike Rout是正确的(但不需要添加侮辱)。添加!= 0是一种浪费,无论如何都会被优化。这不是c“真正”检查。
额外 作者 user223083,

即使能够做到这一点也是一个非常糟糕的主意。它通俗地称为“世界上最后的虫子”,如下所示:

if (alert = CODE_RED)
{
   launch_nukes();
}

虽然你不太可能犯错误相当那么严重,但很容易意外地搞砸并导致代码库中难以发现的错误。大多数现代编译器都会在条件内插入一个警告。他们在那里是有原因的,你应该注意他们,并避免这种结构。

2
额外
在发出这些警告之前,我们会编写 CODE_RED = alert ,因此它会产生编译错误。
额外 作者 user1114,
@ user949300两个字:斯德哥尔摩综合症:P
额外 作者 JanC,
@Ian Yoda被称为条件的。他们很难读。不幸的是他们的必要性。
额外 作者 JanC,
经过一个非常简短的介绍“习惯它”之后,尤达的条件并不比普通条件更难阅读。有时它们更具可读性。例如,如果你有一系列ifs/elseifs,那么在左边测试条件以获得更高的重点是IMO略有改进。
额外 作者 MrG,

两种风格都很好,正确和适当。哪一个更合适在很大程度上取决于您公司的风格指南。现代IDE将通过使用实时语法linting来促进两种样式的使用,这些内容明确地突出了可能会成为混淆源的区域。

例如, Netbeans 突出显示以下表达式:

if($a = someFunction())

以“意外转让”为由。

enter image description here

要明确地告诉Netbeans“是的,我真的打算这样做......”,表达式可以包含在一组括号中。

if(($a = someFunction()))

enter image description here

在一天结束时,这一切都归结为公司风格指南和现代工具的可用性,以促进开发过程。

2
额外