Меня зовут Андрей Карпов. Я изучил сотни ошибок, вызванных «copy-paste». Можно с уверенностью сказать, что программисты часто делают ошибки в последнем блоке большого фрагмента кода. Похоже, ни одна книга по программированию не обсуждала это явление, поэтому я решил написать об этом сам. Я называю это «эффектом последней строки».

copy-paste

Эффект последней строки

Во время программирования программистам часто приходится писать серию похожих структур. Набирать их строка за строкой скучно и неэффективно. Поэтому они используют приём «copy-paste»: фрагмент кода копируется и вставляется несколько раз, а затем изменяется. Всем известен недостаток этого: легко забыть что-то изменить после вставки, что в итоге порождает ошибки. К сожалению, часто нет лучшего способа.

Итак, какую закономерность я обнаружил? Я обнаружил, что ошибки часто возникают в последнем вставленном блоке кода.

Вот краткий пример:

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

Обратите внимание на эту строку: «z += other.y;». Программист забыл заменить 'y' на 'z'.

Вы можете подумать, что это надуманный пример, но на самом деле он взят из реального приложения. Далее я убежу вас, что это распространённая и часто встречающаяся ошибка. Программисты часто делают эту ошибку в конце серии похожих операций.

Я слышал, что скалолазы часто срываются в последних нескольких десятках метров. Не потому, что они устали, а именно потому, что они перевозбуждены приближающимся финишем; они представляют радость успеха, становятся невнимательными и в конце концов срываются. Полагаю, программисты такие же.

Теперь давайте посмотрим на набор данных.

Изучив базу данных, я выделил 84 фрагмента кода, созданных с помощью приёма copy-paste. В 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.

Таким образом, 41 ошибка равномерно распределена между первыми 4 блоками кода, в среднем по 10 ошибок на блок.

Однако последний блок содержит 43 ошибки!

Диаграмма распределения ниже иллюстрирует это явление:

copy-paste-1

Обзорная диаграмма распределения ошибок в пяти похожих фрагментах кода

Следовательно, можно сформулировать правило:

Вероятность допустить ошибку в последнем вставленном блоке кода в 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' всегда равно 1. Последняя строка должна быть:

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

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;
}

В последней строке программист забыл две вещи. Во-первых, ">=" следует заменить на "<=", а во-вторых, знак минус следует заменить на знак плюс.

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))

В последнем qlsFinite переданный аргумент должен быть '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, допускают такие ошибки.

Я надеюсь, что это открытие явления будет полезно программистам и, возможно, побудит их изучить нашу базу данных ошибок. Я верю, что это поможет найти новые закономерности в этих ошибках и сформулировать новые рекомендации по программированию.

Эта статья воспроизведена из: http://www.vaikan.com/the-last-line-effect/