Me llamo Andrey Karpov. He investigado cientos de errores causados por "copiar y pegar". Es seguro afirmar que los programadores suelen equivocarse en el último bloque de un gran fragmento de código. Parece que ningún libro de programación ha tratado este fenómeno, así que decidí escribir algo al respecto. Lo llamo "el efecto de la última línea".

El efecto de la última línea
Al programar, los programadores a menudo necesitan escribir una serie de estructuras similares. Escribir línea por línea es aburrido e ineficiente. Por eso usan la técnica de "copiar y pegar": se copia y pega un fragmento de código varias veces y luego se modifica. Todos saben lo malo de esto: es fácil olvidarse de modificar algo después de pegarlo, lo que acaba generando problemas. Desafortunadamente, a menudo no se encuentra un método mejor.
¿Qué patrón descubrí entonces? Descubrí que los errores suelen ocurrir en el último bloque de código pegado.
A continuación, un breve ejemplo:
inline Vector3int32& operator+=(const Vector3int32& other) {
x += other.x;
y += other.y;
z += other.y;
return *this;
}
Observe esta línea: "z += other.y;". El programador olvidó reemplazar 'y' por 'z'.
Quizás pienses que es un ejemplo hipotético, pero en realidad proviene de una aplicación real. A continuación, te haré creer que es un error común y frecuente. Los programadores suelen cometer este tipo de error al final de una serie de operaciones similares.
He oído que los escaladores suelen resbalar en los últimos metros. No porque estén cansados, sino precisamente por la emoción de estar cerca de la meta; se imaginan la alegría del éxito, se vuelven descuidados y finalmente resbalan. Supongo que a los programadores les pasa igual.
Veamos ahora un conjunto de datos.
Después de estudiar la base de datos, aislé 84 fragmentos de código generados con la técnica de "copiar y pegar". En 41 de ellos, el error ocurría en algún bloque pegado del medio. Por ejemplo:
strncmp(argv[argidx], "CAT=", 4) &&
strncmp(argv[argidx], "DECOY=", 6) &&
strncmp(argv[argidx], "THREADS=", 6) &&
strncmp(argv[argidx], "MINPROB=", 8)) {
La longitud de la cadena "THREADS=" es de 8 caracteres, no 6.
En los otros 43 fragmentos, el error ocurría en el último bloque pegado.
Por supuesto, 43 no es mucho más que 41. Pero nótese que en un programa puede haber muchos bloques de código similares, por lo que el error podría ocurrir en el primero, el segundo, el quinto o incluso el décimo bloque. Así, en los otros bloques tenemos una distribución relativamente uniforme, mientras que en el último bloque hay un pico.
En promedio, el número total de bloques de código similares es 5.
Así, en los primeros 4 bloques se distribuyen uniformemente 41 errores, con un promedio de 10 errores por bloque.
Sin embargo, ¡en el último bloque hay 43 errores!
El siguiente gráfico de distribución resalta este fenómeno:

Gráfico de distribución de errores en cinco bloques de código similares
Por lo tanto, podemos resumir una regla:
La probabilidad de cometer un error en el último bloque pegado es 4 veces mayor que en los otros bloques.
Esta regla puede no ser universal. Es solo un hallazgo interesante, y su utilidad práctica es recordarte que debes mantener la alerta al escribir el último bloque.
Ejemplo:
A continuación voy a demostrar que esto no es una idea descabellada mía, sino que hay una tendencia real. Mire los siguientes ejemplos.
Por supuesto, no enumeraré todos los ejemplos, solo los simples y representativos.
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 );
}
La última línea debería ser 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;
Las dos últimas líneas son iguales.
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 ();
....
}
La última línea de código redundante proviene de pegar por inercia. El tamaño del arreglo es 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));
El programador olvidó cambiar 'BackgroundColor.y' a 'BackgroundColor.z' en la última línea.
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 ) ... }
....
}
El programador olvidó cambiar 'prob > max6' a 'prob > max7' en la última comprobación.
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];
}
La última línea debería usar 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
}
Al último bloque le falta 'patternRepeatX'. El código correcto debería ser:
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' siempre es igual a 1. La última línea debería ser:
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")) { <<<---
El 'ftp' final es sospechoso; ya había sido comparado antes.
Quake-III-Arena
if (fabs(dir[0]) > test->radius ||
fabs(dir[1]) > test->radius ||
fabs(dir[1]) > test->radius)
Se olvidó verificar el valor de dir
Clang
return (ContainerBegLine <= ContaineeBegLine &&
ContainerEndLine <= ContaineeEndLine &&
(ContainerBegLine != ContaineeBegLine ||
SM.getExpansionColumnNumber(ContainerRBeg) <=
SM.getExpansionColumnNumber(ContaineeRBeg)) &&
(ContainerEndLine != ContaineeEndLine ||
SM.getExpansionColumnNumber(ContainerREnd) >=
SM.getExpansionColumnNumber(ContainerREnd)));
En el último bloque, la expresión 'SM.getExpansionColumnNumber(ContainerREnd)' se compara consigo misma.
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;
}
El programador olvidó la 'r' en la última línea.
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;
}
En la última línea, el programador olvidó dos cosas. Primero, '>=' debería cambiarse a '<=', y segundo, el signo menos debería cambiarse a signo más.
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))
En el último qlsFinite, el parámetro pasado debería ser '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;
La cadena 'BITLIST' tiene una longitud de 7, no 3.
Voy a parar aquí. Los ejemplos que he dado ya son suficientes para ilustrar el punto, ¿no?
Conclusión
Este artículo te muestra que la probabilidad de error con la técnica de "copiar y pegar" en el último bloque pegado es probablemente 4 veces mayor que en otros bloques.
Esto está relacionado con la psicología humana, no con el nivel técnico. El artículo muestra que incluso los programadores expertos en proyectos como Clang o Qt cometen este tipo de errores.
Espero que el descubrimiento de este fenómeno sea útil para los programadores, y quizás los anime a investigar nuestra base de datos de errores. Creo que esto ayudará a encontrar nuevos patrones en estos errores y a resumir nuevos consejos de programación.
Este artículo está reproducido de: http://www.vaikan.com/the-last-line-effect/