Рассказывает автор блога HOW NOT TO CODE
Я повидал много ошибок, связанных с методом «Копировать-вставить», и из всех них извлек один вывод: в большинстве случаев ошибка допускается в последнем копипастном фрагменте. Я ни разу не видел описания этого феномена в книгах и поэтому дал ему название сам — «эффект последней строки».
Введение
В процессе анализа всевозможных проектов найденные там баги я сохранял вместе с фрагментами кода в специальной базе. На нее может взглянуть любой желающий вот здесь.
Эта база по-своему уникальна. В ней содержится около 1500 всевозможных ошибок, которые могут стать хорошим уроком для начинающих программистов.
Я не проводил особо сложного анализа этих материалов, однако один паттерн ошибок просматривается настолько четко, что я не могу о нем не рассказать. В своих статьях я всегда повторяю: «Последнюю строку кода нужно печатать». Сейчас я объясню, почему.
Эффект последней строчки
При написании кода программистам часто приходится писать серии одинаковых конструкций. Писать почти одно и то же несколько раз подряд утомительно и неэффективно. Именно потому люди и используют метод копирования-вставки: фрагмент кода копируется и вставляется несколько раз подряд, а потом немного редактируется. Любой понимает опасности этого метода: легко можно забыть внести нужные изменения, что повлечет за собой ошибку. К сожалению, вменяемой альтернативы этому методу в таком случае я не знаю.
Теперь поговорим о закономерности, которую я обнаружил. Я обратил внимание на то, что большинство ошибок допускается в последнем скопированном и вставленном участке кода.
Вот простой пример:
inline Vector3int32& operator+=(const Vector3int32& other) {
x += other.x;
y += other.y;
z += other.y;.
return *this;
}
Обратите внимания на строчку z += other.y;
. Программист забыл исправить y
на z
.
Можно подумать, что это искусственный пример, но нет. Я взял его из реального приложения. В этой статье я собираюсь доказать, что на самом деле это очень распространенная и важная ошибка. Программисты чаще всего ошибаются в самом конце последовательности однотипных действий.
Где-то я слышал, что альпинисты часто срываются на последних десятках метров своего восхождения. Не потому, что они устали: они просто слишком обрадованы тем, что вершина почти достигнута — они уже чувствуют сладкий вкус победы, теряют концентрацию и совершают фатальную ошибку. Я думаю, что с программистами случается примерно то же самое.
Вот еще примеры.
Пролистывая свою базу багов, я нашел 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. Но тут важно учитывать: однородных блоков много. Во всем 41 случае они могут находиться в первом, втором, пятом или десятом блоке. Получается, что мы получаем относительно равномерное распределение ошибок по всей длине блока и резкий пик в конце.
Допустим, что длина нашего однородного блока равна 5. Получается, что в первых четырех блоках будет 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;
Последний блок идентичен предпоследнему.
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
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;
}
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[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)
сравнивается с самим собой.
Выводы
Из этой статьи вы узнали, в чем опасность метода копипаста — ошибка чаще всего совершается в самом конце. Я думаю, причина этого кроется в человеческой психологии, а не профессиональных навыках. То, что вы увидели выше, написали не новички, а высокопрофессиональные разработчики проектов вроде Clang или Qt.
Надеюсь, мои наблюдения пригодятся вам и помогут снизить число багов, пополняющих мою коллекцию.
Источник: блог How Not To Code