歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

編程中的“末行效應”

我研究過數百個因“拷貝-粘貼”導致的錯誤。可以肯定的是,程序員常常會在一大段代碼的最後一段裡犯錯。好像還沒有任何編程書討論過這種現象,因此我決定自己寫點什麼。我稱之為“末行效應”。

我叫Andrey Karpov,我的工作有點不尋常:我借助靜態分析工具研究各種應用程序代碼,並描述從中找到的錯誤或者缺陷。我這麼做既有實際效益也因為工作需要。使用的方法正是基於我們公司所推廣的PVS-Studio和CppCat工具的原理。套路很簡單:找bug,然後寫文章分析bug,文章吸引到潛在用戶的注意,接著就是收益。但今天這篇文章不是介紹這些工具的。

在分析各種軟件項目的過程中,我把找到的bug以及相關代碼存入一個特殊的數據庫。順便說一下,有興趣的話各位可以看一看這個數據庫。我們把它轉換成網頁格式並上傳到了公司網站的“Detected errors”欄下。

這個數據庫獨一無二!目前它收錄了1500塊問題代碼片,正等著程序員們去研究,從中總結出特定規律。為將來的研究,手冊和文章奠定一個基礎。

我還沒認真地分析過目前搜集到的材料。但是過程中我發現有一個明顯的模式反復出現,決定深入研究一下。你大概看到了,文中我反復使用短語“注意最後一行”。在我看來,這一定有某種規律。

末行效應

編程的時候,程序員常常需要寫一系列相似的結構。逐行敲鍵盤輸入無聊且低效。這就是為什麼他們會使用奧義-“拷貝-粘貼”大法:一段代碼被拷貝粘貼幾次,然後修改。誰都知道這樣做的壞處:你很容易在粘貼後忘記修改某些內容最後滋生出問題。不幸的是,常常找不到比這更好的方法。

那麼我發現了什麼規律呢?我發現錯誤常常發生在最後的一塊粘貼代碼裡。

下面是一個簡短的例子:

  1. inlineVector3int32&operator+=(constVector3int32& 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. inlinevoidInit(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. classCWaterPolySAInterface
  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. inlinetypenameValue<Pipe>::Typeconst&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. }elseif(repetition ==QStringLiteral("repeat-x")){
  6. pattern->patternRepeatX =true;
  7. }elseif(repetition ==QStringLiteral("repeat-y")){
  8. pattern->patternRepeatY =true;
  9. }elseif(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. constint istride =sizeof(tmp[0])/sizeof(tmp[0][0][0]);
  2. constint jstride =sizeof(tmp[0][0])/sizeof(tmp[0][0][0]);
  3. constint mistride =sizeof(mag[0])/sizeof(mag[0][0]);
  4. constint mjstride =sizeof(mag[0][0])/sizeof(mag[0][0]);

‘mjstride’永遠等於1。最後一行應該是:

  1. constint 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. booloperator==(constMemberCfg& 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. staticboolPositionIsInside(....)
  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. elseif(!strncmp(vstart,"UTF8",4))
  4. arg->format = ASN1_GEN_FORMAT_UTF8;
  5. elseif(!strncmp(vstart,"HEX",3))
  6. arg->format = ASN1_GEN_FORMAT_HEX;
  7. elseif(!strncmp(vstart,"BITLIST",3))
  8. arg->format = ASN1_GEN_FORMAT_BITLIST;

字符串“BITLIST”長度為7,而非3。

就此打住吧。我舉的例子已經夠說明問題了吧?

結論

本文告訴你“拷貝-粘貼”大法在最後一個粘貼代碼塊中出錯的概率很可能是其他塊的4倍。

這跟人類的心理學有關,與技術水平無關。文中說明了即便是像Clang或者Qt項目中的編程高手也會犯這種錯誤。

我希望這個現象的發現對於程序員們有所幫助,也許可以促使他們去研究我們的bug數據庫。相信如此有助於在這些錯誤中發現新的規律並總結出新���編程建議。

原文: http://www.viva64.com/en/b/0260/print/   
譯文: http://www.vaikan.com/the-last-line-effect/    譯者: Ryan Chen

Copyright © Linux教程網 All Rights Reserved