作者:
2014 年 5 月 31 日 (最後更新:2014 年 5 月 31 日)

最後一行效應

評分:4.3/5 (159 票)
*****

我研究了由於使用複製貼上方法而導致的許多錯誤,並可以向您保證,程式設計師最常犯的錯誤是在同質程式碼塊的最後一部分。我從未在程式設計書籍中見過這種現象的描述,所以我決定自己寫一篇關於它的文章。我稱之為“最後一行效應”。

簡介

我叫 Andrey Karpov,我做一份不尋常的工作——我透過靜態分析器分析各種應用程式的程式程式碼,並撰寫我發現的錯誤和缺陷的描述。我這樣做是出於務實和功利的原因,因為我的工作是我們公司宣傳其工具 PVS-Studio 和 CppCat 的方式。這個模式很簡單。我找到 Bug。然後我描述它們。文章吸引了我們潛在客戶的注意力。盈利。但今天的文章不是關於分析器。

在進行各種專案的分析時,我會將我發現的 Bug 和相應的程式碼片段儲存在一個特殊的資料庫中。順便說一下,任何有興趣的人都可以看看這個資料庫。我們將其轉換為一系列 html 頁面,並將其上傳到我們網站的“檢測到的錯誤”部分。

這個資料庫確實是獨一無二的!它目前包含 1500 個帶有錯誤的 कोड 片段,並等待程式設計師學習它並揭示這些錯誤中的某些規律。這可以為未來的許多研究、手冊和文章提供有用的基礎。

我從來沒有對收集到的材料進行過任何特別的調查。然而,一個模式非常清晰,以至於我決定深入研究一下。您知道,在我的文章中,我經常不得不寫“注意最後一行”這句話。我想到一定有什麼原因。

最後一行效應

在編寫程式程式碼時,程式設計師經常需要編寫一系列相似的結構。多次輸入相同的程式碼既乏味又效率低下。這就是為什麼他們使用複製貼上方法:程式碼片段被複制並貼上幾次,然後進行進一步編輯。每個人都知道這種方法的壞處:您很容易忘記在貼上的行中更改某些內容,從而產生錯誤。不幸的是,通常沒有更好的替代方法。

現在我們來談談我發現的模式。我發現錯誤最常發生在最後一個貼上的程式碼塊中。

這是一個簡單而短的例子

inline Vector3int32& operator+=(const Vector3int32& other) {
  x += other.x;
  y += other.y;
  z += other.y;
  return *this;
}

注意“z += other.y;”這一行。程式設計師忘記將 'y' 替換為 'z'。

您可能會認為這是一個人為的示例,但它實際上來自一個真實應用程式。在本文的後面,我將說服您這是一個非常頻繁和常見的問題。這就是“最後一行效應”的樣子。程式設計師最常在相似編輯序列的末尾犯錯。

我曾經在哪裡聽說過登山者在攀登的最後幾十米常常會摔下來。不是因為他們累了;他們只是對幾乎到達頂峰感到過於高興——他們期待勝利的甜蜜滋味,注意力變得不那麼集中,然後犯下一些致命的錯誤。我想程式設計師也發生了一些類似的事情。

現在說點數字。

在研究了 Bug 資料庫後,我挑出了 84 個我發現是透過複製貼上方法編寫的程式碼片段。其中,41 個片段在複製貼上塊的中間包含錯誤。例如

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

“THREADS=”字串的長度是 8 個字元,而不是 6 個。

在其他 43 個案例中,錯誤發現在最後一個複製的程式碼塊中。

好吧,43 這個數字看起來只比 41 稍微大一點。但請記住,同質塊可能有很多,所以錯誤可能出現在第一個、第二個、第五個,甚至第十個塊中。這樣我們就得到了一個錯誤在塊中相對平滑的分佈,以及一個在結尾的尖銳峰值。

我平均將同質塊的數量視為 5。

所以,看起來前 4 個塊包含 41 個錯誤,分佈在它們之中;這使得每個塊大約有 10 個錯誤。

剩下 43 個錯誤留給第五個塊!

所以我們得到的是這樣的模式

在最後貼上的程式碼塊中犯錯的機率是其他任何程式碼塊的 4 倍。

我不會從中得出任何宏大的結論。這只是一個有趣的觀察,瞭解它可能很有用——當您編寫最後程式碼片段時,您會保持警惕。

例子

現在我只需要說服讀者,這一切都不是我的奇思妙想,而是真實的趨勢。為了證明我的話,我將向您展示一些例子。

我當然不會引用所有例子——只會引用最簡單或最有代表性的。

Source Engine SDK

inline void Init( float ix=0, float iy=0,
                  float iz=0, float iw = 0 ) 
{
  SetX( ix );
  SetY( iy );
  SetZ( iz );
  SetZ( iw );
}

SetW() 函式應該在最後呼叫。

Chromium

if (access & FILE_WRITE_ATTRIBUTES)
  output.append(ASCIIToUTF16("\tFILE_WRITE_ATTRIBUTES\n"));
if (access & FILE_WRITE_DATA)
  output.append(ASCIIToUTF16("\tFILE_WRITE_DATA\n"));
if (access & FILE_WRITE_EA)
  output.append(ASCIIToUTF16("\tFILE_WRITE_EA\n"));
if (access & FILE_WRITE_EA)
  output.append(ASCIIToUTF16("\tFILE_WRITE_EA\n"));
break;

最後一個塊和前一個塊是相同的。

ReactOS

if (*ScanString == L'\"' ||
    *ScanString == L'^' ||
    *ScanString == L'\"')

Multi Theft Auto

class CWaterPolySAInterface
{
public:
    WORD m_wVertexIDs[3];
};
CWaterPoly* CWaterManagerSA::CreateQuad (....)
{
  ....
  pInterface->m_wVertexIDs [ 0 ] = pV1->GetID ();
  pInterface->m_wVertexIDs [ 1 ] = pV2->GetID ();
  pInterface->m_wVertexIDs [ 2 ] = pV3->GetID ();
  pInterface->m_wVertexIDs [ 3 ] = pV4->GetID ();
  ....
}

最後一行是機械貼上的,是多餘的。陣列只有 3 個元素。

Source Engine SDK

intens.x=OrSIMD(AndSIMD(BackgroundColor.x,no_hit_mask),
                AndNotSIMD(no_hit_mask,intens.x));
intens.y=OrSIMD(AndSIMD(BackgroundColor.y,no_hit_mask),
                AndNotSIMD(no_hit_mask,intens.y));
intens.z=OrSIMD(AndSIMD(BackgroundColor.y,no_hit_mask),
                AndNotSIMD(no_hit_mask,intens.z));

程式設計師忘記在最後一個塊中將“BackgroundColor.y”替換為“BackgroundColor.z”。

Trans-Proteomic Pipeline

void setPepMaxProb(....)
{  
  ....
  double max4 = 0.0;
  double max5 = 0.0;
  double max6 = 0.0;
  double max7 = 0.0;
  ....
  if ( pep3 ) { ... if ( use_joint_probs && prob > max3 ) ... }
  ....
  if ( pep4 ) { ... if ( use_joint_probs && prob > max4 ) ... }
  ....
  if ( pep5 ) { ... if ( use_joint_probs && prob > max5 ) ... }
  ....
  if ( pep6 ) { ... if ( use_joint_probs && prob > max6 ) ... }
  ....
  if ( pep7 ) { ... if ( use_joint_probs && prob > max6 ) ... }
  ....
}

程式設計師忘記將最後一個條件中的“prob > max6”替換為“prob > max7”。

SeqAn

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

SlimDX

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

rglFSlider 陣列應該在最後一行中使用。

Qt

if (repetition == QStringLiteral("repeat") ||
    repetition.isEmpty()) {
  pattern->patternRepeatX = true;
  pattern->patternRepeatY = true;
} else if (repetition == QStringLiteral("repeat-x")) {
  pattern->patternRepeatX = true;
} else if (repetition == QStringLiteral("repeat-y")) {
  pattern->patternRepeatY = true;
} else if (repetition == QStringLiteral("no-repeat")) {
  pattern->patternRepeatY = false;
  pattern->patternRepeatY = false;
} else {
  //TODO: exception: SYNTAX_ERR
}

'patternRepeatX' 在最後一個塊中丟失。正確的程式碼如下

pattern->patternRepeatX = false;
pattern->patternRepeatY = false;

ReactOS

const int istride = sizeof(tmp[0]) / sizeof(tmp[0][0][0]);
const int jstride = sizeof(tmp[0][0]) / sizeof(tmp[0][0][0]);
const int mistride = sizeof(mag[0]) / sizeof(mag[0][0]);
const int mjstride = sizeof(mag[0][0]) / sizeof(mag[0][0]);

“mjstride”變數將始終等於一。最後一行應該這樣寫

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

Mozilla Firefox

if (protocol.EqualsIgnoreCase("http") ||
    protocol.EqualsIgnoreCase("https") ||
    protocol.EqualsIgnoreCase("news") ||
    protocol.EqualsIgnoreCase("ftp") ||          <<<---
    protocol.EqualsIgnoreCase("file") ||
    protocol.EqualsIgnoreCase("javascript") ||
    protocol.EqualsIgnoreCase("ftp")) {          <<<---

結尾有一個可疑的字串“ftp”——它已經被比較過了。

Quake-III-Arena

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

dir[2] 單元格的值未經驗證。

Clang

return (ContainerBegLine <= ContaineeBegLine &&
        ContainerEndLine >= ContaineeEndLine &&
        (ContainerBegLine != ContaineeBegLine ||
         SM.getExpansionColumnNumber(ContainerRBeg) <=
         SM.getExpansionColumnNumber(ContaineeRBeg)) &&
        (ContainerEndLine != ContaineeEndLine ||
         SM.getExpansionColumnNumber(ContainerREnd) >=
         SM.getExpansionColumnNumber(ContainerREnd)));

在塊的末尾,“SM.getExpansionColumnNumber(ContainerREnd)”表示式與自身進行比較。

MongoDB

bool operator==(const MemberCfg& r) const {
  ....
  return _id==r._id && votes == r.votes &&
         h == r.h && priority == r.priority &&
         arbiterOnly == r.arbiterOnly &&
         slaveDelay == r.slaveDelay &&
         hidden == r.hidden &&
         buildIndexes == buildIndexes;
}

程式設計師在最後一行忘記了“r.”。

Unreal Engine 4

static bool PositionIsInside(....)
{
  return
    Position.X >= Control.Center.X - BoxSize.X * 0.5f &&
    Position.X <= Control.Center.X + BoxSize.X * 0.5f &&
    Position.Y >= Control.Center.Y - BoxSize.Y * 0.5f &&
    Position.Y >= Control.Center.Y - BoxSize.Y * 0.5f;
}

程式設計師忘記在最後一行進行 2 項編輯。首先,“>=”應替換為“<=;”其次,減號應替換為加號。

Qt

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

在對函式 qIsFinite 的最後一次呼叫中,變數“h”應作為引數使用。

OpenSSL

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

“BITLIST”字串的長度是 7 個字元,而不是 3 個。

我們就到這裡吧。我希望我展示的例子已經足夠多了。

結論

從這篇文章中,您瞭解到使用複製貼上方法,在最後貼上的程式碼塊中犯錯的機率比在其他任何程式碼片段中犯錯的機率高 4 倍。

這與人的心理特點有關,而不是專業技能。我在本文中向您展示了,即使是像 Clang 或 Qt 這樣的專案中的技術嫻熟的開發人員,也傾向於犯此類錯誤。

我希望我的觀察對程式設計師有用,並可能促使他們研究我們的 Bug 資料庫。我相信這將有助於揭示許多錯誤規律,並制定新的程式設計師建議。