分析程序员在代码编程中的“末行效应”

开发 后端 前端
我研究过数百个因“拷贝-粘贴”导致的错误。可以肯定的是,程序员常常会在一大段代码的最后一段里犯错。好像还没有任何编程书讨论过这种现象,因此我决定自己写点什么。我称之为“末行效应”。

我研究过数百个因“拷贝-粘贴”导致的错误。可以肯定的是,程序员常常会在一大段代码的***一段里犯错。好像还没有任何编程书讨论过这种现象,因此我决定自己写点什么。我称之为“末行效应”。

[[122874]]

我叫Andrey Karpov,我的工作有点不寻常:我借助静态分析工具研究各种应用程序代码,并描述从中找到的错误或者缺陷。我这么做既有实际效益也因为工作需要。使用的方法正是基于我们公司所推广的PVS-Studio和CppCat工具的原理。套路很简单:找bug,然后写文章分析bug,文章吸引到潜在用户的注意,接着就是收益。但今天这篇文章不是介绍这些工具的。

在分析各种软件项目的过程中,我把找到的bug以及相关代码存入一个特殊的数据库。顺便说一下,有兴趣的话各位可以看一看这个数据库。我们把它转换成网页格式并上传到了公司网站的“Detected errors”栏下。

这个数据库***!目前它收录了1500块问题代码片,正等着程序员们去研究,从中总结出特定规律。为将来的研究,手册和文章奠定一个基础。

我还没认真地分析过目前搜集到的材料。但是过程中我发现有一个明显的模式反复出现,决定深入研究一下。你大概看到了,文中我反复使用短语“注意***一行”。在我看来,这一定有某种规律。

末行效应

编程的时候,程序员常常需要写一系列相似的结构。逐行敲键盘输入无聊且低效。这就是为什么他们会使用奥义-“拷贝-粘贴”大法:一段代码被拷贝粘贴几次,然后修改。谁都知道这样做的坏处:你很容易在粘贴后忘记修改某些内容***滋生出问题。不幸的是,常常找不到比这更好的方法。

那么我发现了什么规律呢?我发现错误常常发生在***的一块粘贴代码里。

下面是一个简短的例子:

  1. inline Vector3int32& operator+=(const Vector3int32& other) { 
  2.   x += other.x; 
  3.   y += other.y; 
  4.   z += other.y; 
  5.   return *this

注意这一行:”z += other.y;”。程序员忘记把‘y’替换成‘z’了。

也许你以为这是个假设的例子,然后它其实来自一个真实的应用程序。接下来,我会让你相信这是高频常见的一种错误。程序员们经常在一连串相似操作的结尾犯这种错误。

我听说攀岩者常常在***的几十米中滑落下来。并不是因为他们累了,而正是由于他们对即将到达的终点过于兴奋,他们想象着成功后的喜悦,变得疏忽大意,***失足。我猜想程序员们也是这样的。

接下来看一组数据。

研究了数据库后,我分离出了84个代码段由“拷贝-粘贴”大法生成。其中41段中错误发生在中间的某些粘贴块。比如:

  1. strncmp(argv[argidx], "CAT="4) && 
  2. strncmp(argv[argidx], "DECOY="6) && 
  3. strncmp(argv[argidx], "THREADS="6) && 
  4. strncmp(argv[argidx], "MINPROB="8)) { 

“THREADS=”字符串的长度是8个字符,而非6。

另外的43段代码中,错误发生在***的粘贴块。

当然,43比41大不了多少。但是请注意,一段程序中,可能有很多类似的代码块,因此错误可能发生在***,第二,第五甚至第十块中。因此在其他代码块中我们有一个相对均匀的分布,而***一块却存在一个峰值。

平均而言,相似代码块总数为5。

于是前面4个代码块中均匀分布了41处错误,平均每块代码有10个错误。

然而***一块代码中有43个错误!

下面的分布概图凸显出这个现象:

[[122875]]

图1. 五块类似代码段中的错误分布概图

因此我们可以总结出一个规律:

在最末的粘贴代码块中出错的概率是其他代码块的4倍。

这个规律可能并没有普适性。它只是个有趣的发现,其实际效用在于:提醒在你写***一块的时候保持警觉。

实例:

下面我要证明这并不是我的胡思乱想而是有真实的趋势的。请看下面的实例。

当然,我不会列出所有例子,仅列举简单而有代表性的。

Source Engine SDK

  1. inline void Init( float ix=0float iy=0
  2.                   float iz=0float iw = 0 )  
  3.   SetX( ix ); 
  4.   SetY( iy ); 
  5.   SetZ( iz ); 
  6.   SetZ( iw ); 

***一行应该是SetW()。

Chromium

  1. if (access & FILE_WRITE_ATTRIBUTES) 
  2.   output.append(ASCIIToUTF16("\tFILE_WRITE_ATTRIBUTES\n")); 
  3. if (access & FILE_WRITE_DATA) 
  4.   output.append(ASCIIToUTF16("\tFILE_WRITE_DATA\n")); 
  5. if (access & FILE_WRITE_EA) 
  6.   output.append(ASCIIToUTF16("\tFILE_WRITE_EA\n")); 
  7. if (access & FILE_WRITE_EA) 
  8.   output.append(ASCIIToUTF16("\tFILE_WRITE_EA\n")); 
  9. break

***两行相同。

ReactOS

  1. if (*ScanString == L'\"' || 
  2.     *ScanString == L'^' || 
  3.     *ScanString == L'\"'

Multi Theft Auto

  1. class CWaterPolySAInterface 
  2. public
  3.     WORD m_wVertexIDs[3]; 
  4. }; 
  5. CWaterPoly* CWaterManagerSA::CreateQuad (....) 
  6.   .... 
  7.   pInterface->m_wVertexIDs [ 0 ] = pV1->GetID (); 
  8.   pInterface->m_wVertexIDs [ 1 ] = pV2->GetID (); 
  9.   pInterface->m_wVertexIDs [ 2 ] = pV3->GetID (); 
  10.   pInterface->m_wVertexIDs [ 3 ] = pV4->GetID (); 
  11.   .... 

***一行冗余代码来自于惯性粘贴。数组的大小是3。

Source Engine SDK

  1. intens.x=OrSIMD(AndSIMD(BackgroundColor.x,no_hit_mask), 
  2.                 AndNotSIMD(no_hit_mask,intens.x)); 
  3. intens.y=OrSIMD(AndSIMD(BackgroundColor.y,no_hit_mask), 
  4.                 AndNotSIMD(no_hit_mask,intens.y)); 
  5. intens.z=OrSIMD(AndSIMD(BackgroundColor.y,no_hit_mask), 
  6.                 AndNotSIMD(no_hit_mask,intens.z)); 

程序员忘记把***一行的中的“BackgroundColor.y”改成“BackgroundColor.z”。

#p#

Trans-Proteomic Pipeline

  1. void setPepMaxProb(....) 
  2. {   
  3.   .... 
  4.   double max4 = 0.0
  5.   double max5 = 0.0
  6.   double max6 = 0.0
  7.   double max7 = 0.0
  8.   .... 
  9.   if ( pep3 ) { ... if ( use_joint_probs && prob > max3 ) ... } 
  10.   .... 
  11.   if ( pep4 ) { ... if ( use_joint_probs && prob > max4 ) ... } 
  12.   .... 
  13.   if ( pep5 ) { ... if ( use_joint_probs && prob > max5 ) ... } 
  14.   .... 
  15.   if ( pep6 ) { ... if ( use_joint_probs && prob > max6 ) ... } 
  16.   .... 
  17.   if ( pep7 ) { ... if ( use_joint_probs && prob > max6 ) ... } 
  18.   .... 

程序员忘记把***一个判断中的“prob > max6”改为“prob > max7”。

SeqAn

  1. inline typename Value<Pipe>::Type const & operator*() { 
  2.   tmp.i1 = *in.in1; 
  3.   tmp.i2 = *in.in2; 
  4.   tmp.i3 = *in.in2; 
  5.   return tmp; 

SlimDX

  1. forint i = 0; i < 2; i++ ) 
  2.   sliders[i] = joystate.rglSlider[i]; 
  3.   asliders[i] = joystate.rglASlider[i]; 
  4.   vsliders[i] = joystate.rglVSlider[i]; 
  5.   fsliders[i] = joystate.rglVSlider[i]; 

***一行应该用rglFSlider。

Qt

  1. if (repetition == QStringLiteral("repeat") || 
  2.     repetition.isEmpty()) { 
  3.   pattern->patternRepeatX = true
  4.   pattern->patternRepeatY = true
  5. else if (repetition == QStringLiteral("repeat-x")) { 
  6.   pattern->patternRepeatX = true
  7. else if (repetition == QStringLiteral("repeat-y")) { 
  8.   pattern->patternRepeatY = true
  9. else if (repetition == QStringLiteral("no-repeat")) { 
  10.   pattern->patternRepeatY = false
  11.   pattern->patternRepeatY = false
  12. else { 
  13.   //TODO: exception: SYNTAX_ERR 

***一块少了‘patternRepeatX’。正确的代码应该是:

  1. pattern->patternRepeatX = false
  2. pattern->patternRepeatY = false

ReactOS

  1. const int istride = sizeof(tmp[0]) / sizeof(tmp[0][0][0]); 
  2. const int jstride = sizeof(tmp[0][0]) / sizeof(tmp[0][0][0]); 
  3. const int mistride = sizeof(mag[0]) / sizeof(mag[0][0]); 
  4. const int mjstride = sizeof(mag[0][0]) / sizeof(mag[0][0]); 

‘mjstride’永远等于1。***一行应该是:

  1. const int mjstride = sizeof(mag[0][0]) / sizeof(mag[0][0][0]); 

Mozilla Firefox

  1. if (protocol.EqualsIgnoreCase("http") || 
  2.     protocol.EqualsIgnoreCase("https") || 
  3.     protocol.EqualsIgnoreCase("news") || 
  4.     protocol.EqualsIgnoreCase("ftp") ||          <<<--- 
  5.     protocol.EqualsIgnoreCase("file") || 
  6.     protocol.EqualsIgnoreCase("javascript") || 
  7.     protocol.EqualsIgnoreCase("ftp")) {          <<<--- 

***的“ftp”很可疑,它之前已经被比较过了。

Quake-III-Arena

  1. if (fabs(dir[0]) > test->radius || 
  2.     fabs(dir[1]) > test->radius || 
  3.     fabs(dir[1]) > test->radius) 

dir[2]的值忘记检查了。

Clang

  1. return (ContainerBegLine <= ContaineeBegLine && 
  2.         ContainerEndLine <= ContaineeEndLine && 
  3.         (ContainerBegLine != ContaineeBegLine || 
  4.          SM.getExpansionColumnNumber(ContainerRBeg) <= 
  5.          SM.getExpansionColumnNumber(ContaineeRBeg)) && 
  6.         (ContainerEndLine != ContaineeEndLine || 
  7.          SM.getExpansionColumnNumber(ContainerREnd) >= 
  8.          SM.getExpansionColumnNumber(ContainerREnd))); 

***一块,“SM.getExpansionColumnNumber(ContainerREnd)”表达式在跟自己比较大小。

MongoDB

  1. bool operator==(const MemberCfg& r) const { 
  2.   .... 
  3.   return _id==r._id && votes == r.votes && 
  4.          h == r.h && priority == r.priority && 
  5.          arbiterOnly == r.arbiterOnly && 
  6.          slaveDelay == r.slaveDelay && 
  7.          hidden == r.hidden && 
  8.          buildIndexes == buildIndexes; 

程序员把***一行的“r”忘记了。

Unreal Engine 4

  1. static bool PositionIsInside(....) 
  2.   return 
  3.     Position.X >= Control.Center.X - BoxSize.X * 0.5f && 
  4.     Position.X <= Control.Center.X + BoxSize.X * 0.5f && 
  5.     Position.Y >= Control.Center.Y - BoxSize.Y * 0.5f && 
  6.     Position.Y >= Control.Center.Y - BoxSize.Y * 0.5f; 

***一行中,程序员忘记了两个地方。首先,“>=”应改为“<=”,其次,减号应改为加号。

Qt

  1. qreal x = ctx->callData->args[0].toNumber(); 
  2. qreal y = ctx->callData->args[1].toNumber(); 
  3. qreal w = ctx->callData->args[2].toNumber(); 
  4. qreal h = ctx->callData->args[3].toNumber(); 
  5. if (!qIsFinite(x) || !qIsFinite(y) || 
  6.     !qIsFinite(w) || !qIsFinite(w)) 

***一个qlsFinite中,传入参数应该是‘h’。

OpenSSL

  1. if (!strncmp(vstart, "ASCII"5)) 
  2.   arg->format = ASN1_GEN_FORMAT_ASCII; 
  3. else if (!strncmp(vstart, "UTF8"4)) 
  4.   arg->format = ASN1_GEN_FORMAT_UTF8; 
  5. else if (!strncmp(vstart, "HEX"3)) 
  6.   arg->format = ASN1_GEN_FORMAT_HEX; 
  7. else if (!strncmp(vstart, "BITLIST"3)) 
  8.   arg->format = ASN1_GEN_FORMAT_BITLIST; 

字符串“BITLIST”长度为7,而非3。

就此打住吧。我举的例子已经够说明问题了吧?

结论

本文告诉你“拷贝-粘贴”大法在***一个粘贴代码块中出错的概率很可能是其他块的4倍。

这跟人类的心理学有关,与技术水平无关。文中说明了即便是像Clang或者Qt项目中的编程高手也会犯这种错误。

我希望这个现象的发现对于程序员们有所帮助,也许可以促使他们去研究我们的bug数据库。相信如此有助于在这些错误中发现新的规律并总结出新的编程建议。

责任编辑:张伟 来源: 外刊IT评论
相关推荐

2012-06-15 09:54:58

程序员编程开发

2012-08-29 15:49:50

2009-02-17 10:12:10

ASP面试技术

2012-08-30 10:05:40

编程编程语言程序员

2014-12-23 11:16:43

程序员

2017-03-13 09:07:05

测定代码基因

2013-11-14 10:05:25

程序员职业转型

2015-02-03 02:40:33

程序员盲人程序员

2014-11-10 09:46:57

程序员

2020-10-27 11:43:29

低代码开发工具开发

2021-03-29 23:05:36

程序员工具静态分析

2020-12-07 14:46:07

程序员代码分析工具

2013-06-28 13:28:09

2014-06-13 11:03:19

程序员

2013-08-20 09:33:59

程序员

2019-06-17 01:23:06

程序员技能开发者

2014-08-01 10:18:16

.Netdump

2011-11-11 16:20:22

编程

2015-06-17 14:24:48

优秀程序员整洁代码

2011-06-03 09:58:03

结对编程程序员
点赞
收藏

51CTO技术栈公众号