Меня зовут Андрей Карпов. Я изучил сотни ошибок, вызванных «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 ошибки!
Диаграмма распределения ниже иллюстрирует это явление:

Обзорная диаграмма распределения ошибок в пяти похожих фрагментах кода
Следовательно, можно сформулировать правило:
Вероятность допустить ошибку в последнем вставленном блоке кода в 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/