编程中的“末行效应”

简介:

编程中的“末行效应”

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

我叫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;
  6. }

注意这一行:”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个错误!

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

 

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

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

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

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

实例:

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

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

Source Engine SDK

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

最后一行应该是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. {
  3. public:
  4. WORD m_wVertexIDs[3];
  5. };
  6. CWaterPoly* CWaterManagerSA::CreateQuad (....)
  7. {
  8. ....
  9. pInterface->m_wVertexIDs [ 0 ] = pV1->GetID ();
  10. pInterface->m_wVertexIDs [ 1 ] = pV2->GetID ();
  11. pInterface->m_wVertexIDs [ 2 ] = pV3->GetID ();
  12. pInterface->m_wVertexIDs [ 3 ] = pV4->GetID ();
  13. ....
  14. }

最后一行冗余代码来自于惯性粘贴。数组的大小是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”。

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. ....
  19. }

程序员忘记把最后一个判断中的“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;
  6. }

SlimDX

 
 
  1. for( int i = 0; i < 2; i++ )
  2. {
  3. sliders[i] = joystate.rglSlider[i];
  4. asliders[i] = joystate.rglASlider[i];
  5. vsliders[i] = joystate.rglVSlider[i];
  6. fsliders[i] = joystate.rglVSlider[i];
  7. }

最后一行应该用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
  14. }

最后一块少了‘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;
  9. }

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

Unreal Engine 4

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

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

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数据库。相信如此有助于在这些错误中发现新的规律并总结出新的编程建议。

原文发布时间:2014-11-22

本文来自云栖合作伙伴“linux中国”

目录
相关文章
|
4月前
|
人工智能 数据挖掘 程序员
代码力量:探寻编程在现代社会中的无限可能性
代码力量:探寻编程在现代社会中的无限可能性
|
6月前
|
C++
思维模型No5|网络效应--快速增长的奥秘
思维模型No5|网络效应--快速增长的奥秘
66 1
|
16天前
|
程序员 开发者
代码与禅意:寻找编程中的内在和谐
【4月更文挑战第8天】 在数字世界的纷繁复杂中,编程不仅仅是一项技能,更是一种艺术。本文探讨了编程与禅宗哲学之间的深层联系,阐述了如何在技术的冰冷逻辑中寻找到心灵的温暖和内在的和谐。通过反思编程过程中的创造力、专注力和简洁性,我们能够揭示出技术实践与精神修养之间的桥梁。
|
2月前
|
测试技术
框架效应
框架效应
|
5月前
|
自然语言处理 算法 数据挖掘
什么是知识学习的复利效应
什么是知识学习的复利效应
32 0
|
9月前
|
敏捷开发 数据可视化 安全
软件开发中的破窗效应
软件开发中的破窗效应
|
负载均衡 监控 测试技术
认清性能问题
首先专注于业务上最需要优先修正的程序,而不是从全局调优来改善性能。要重视全局的性能表现,但解决问题要从细节和业务最需要的环节入手。
认清性能问题
|
Java 开发者 微服务
服务雪崩效应|学习笔记
快速学习服务雪崩效应
81 0
服务雪崩效应|学习笔记
|
架构师 安全 搜索推荐
技术人员如何破除达克效应(认知偏差)?
技术人员如何破除达克效应(认知偏差)?
266 0
技术人员如何破除达克效应(认知偏差)?
|
存储 传感器 人工智能
从现实世界的角度去理解计算机领域的知识
​ “ 从现实世界的角度看计算机,可以帮助我们更清晰的学习计算机领域的知识。 本文试图用生活中常见的事物来解释计算机技术领域,让知识更通俗易懂 ”
871 0
 从现实世界的角度去理解计算机领域的知识