Efecto de última línea

Efecto de última línea

He estudiado muchos errores causados ​​por el uso del método Copiar y Pegar, y puedo asegurarles que los programadores suelen cometer errores en el último fragmento de un bloque de código homogéneo. Nunca había visto este fenómeno descrito en libros de programación, así que decidí escribir sobre ello yo mismo. Lo llamé el "efecto de última línea".


Introducción

Al realizar análisis en varios proyectos, guardo los errores que encuentro, junto con los fragmentos de código correspondientes, en una base de datos especial. Por cierto, cualquiera que esté interesado puede echar un vistazo a esta base de datos. Lo convertimos en una colección de páginas html y las subimos a nuestro sitio web en la sección "Errores detectados".

¡Esta base de datos es realmente única! Actualmente contiene 12000 fragmentos de código que contienen errores y está esperando que los programadores lo estudien y revelen patrones regulares entre estos errores. Eso puede servir como una base útil para muchas investigaciones futuras, junto con muchos manuales y artículos.

Nunca he llevado a cabo ninguna investigación especial del material que he reunido en todo este tiempo. Sin embargo, un patrón está apareciendo tan claramente que decidí investigarlo un poco más a fondo. Verá, en mis artículos tengo que escribir la frase "note la última línea" con bastante frecuencia. Se me ocurrió que tenía que haber una razón detrás de esto.

Efecto de última línea

Al escribir código de programa, los programadores a menudo tienen que escribir una serie de construcciones similares. Escribir el mismo código varias veces es aburrido e ineficiente. Es por eso que usan el método Copiar y Pegar:un fragmento de código se copia y pega varias veces, con edición adicional. Todo el mundo sabe lo que tiene de malo este método:corre el riesgo de olvidarse fácilmente de cambiar algo en las líneas pegadas y, por lo tanto, generar errores. Desafortunadamente, a menudo no se puede encontrar una alternativa mejor.

Ahora hablemos del patrón que descubrí. Descubrí que los errores se cometen con mayor frecuencia en el último bloque de código pegado.

Aquí hay un ejemplo simple y breve:

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

Tenga en cuenta la línea "z +=otro.y;". El programador olvidó reemplazar 'y' con 'z'.

Puede pensar que se trata de una muestra artificial, pero en realidad se tomó de una aplicación real. Más adelante en este artículo, te voy a convencer de que este es un tema muy frecuente y común. Así es como se ve el "efecto de última línea". Los programadores suelen cometer errores al final de una secuencia de ediciones similares.

Escuché en alguna parte que los alpinistas a menudo se caen en las últimas docenas de metros de su ascenso. No porque estén cansados; simplemente están demasiado contentos de estar a punto de llegar a la cima:anticipan el dulce sabor de la victoria, se vuelven menos atentos y cometen algún error fatal. Supongo que les pasa algo similar a los programadores.

Ahora algunas cifras.

Después de estudiar la base de datos de errores, seleccioné 84 fragmentos de código que descubrí que se habían escrito mediante el método Copiar y Pegar. De ellos, 41 fragmentos contienen errores en algún lugar en medio de los bloques copiados y pegados. 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 de 6.

En los otros 43 casos, se encontraron errores en el último bloque de código copiado.

Bueno, el número 43 parece un poco más grande que el 41. Pero tenga en cuenta que puede haber muchos bloques homogéneos, por lo que se pueden encontrar errores en el primer, segundo, quinto o incluso décimo bloque. Por lo tanto, obtenemos una distribución relativamente suave de errores a lo largo de los bloques y un pico pronunciado al final.

Acepté que el número de bloques homogéneos sea 5 en promedio.

Entonces parece que los primeros 4 bloques contienen 41 errores distribuidos a lo largo de ellos; eso comete alrededor de 10 errores por bloque.

¡Y quedan 43 errores para el quinto bloque!

Para hacerlo más claro, aquí hay un diagrama aproximado:

Figura 1. Un diagrama aproximado de la distribución de errores en cinco bloques de código homogéneo.

Entonces, lo que obtenemos es el siguiente patrón:

La probabilidad de cometer un error en el último bloque de código pegado es 4 veces mayor que en cualquier otro bloque.

No saco grandes conclusiones de eso. Es solo una observación interesante que puede ser útil conocer, por razones prácticas:permanecerá alerta cuando escriba los últimos fragmentos de código.

Ejemplos

Ahora solo me queda convencer a los lectores de que todo esto no es mi fantasía, sino una tendencia real. Para probar mi punto, te mostraré algunos ejemplos.

No citaré todos los ejemplos, por supuesto, solo los más simples o los más representativos.

SDK de motor de origen

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

La función SetW() debe llamarse al final.

Cromo

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;

El último bloque y el anterior son idénticos.

ReactOS

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

Robo múltiple de automóviles

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 se pegó mecánicamente y es redundante. Solo hay 3 elementos en la matriz.

SDK de motor de origen

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ó reemplazar "BackgroundColor.y" con "BackgroundColor.z" en el último bloque.

Transporte transproteómico

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ó reemplazar "prob> max6" con "prob> max7" en la última condición.

SeqAn

inline typename Value::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 matriz rglFSlider debería haberse utilizado en la última línea.

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
}

Falta ‘patternRepeatX’ en el último bloque. El código correcto es el siguiente:

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

La variable 'mjstride' siempre será igual a uno. La última línea debería haberse escrito así:

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")) {          // <=

Una cadena sospechosa "ftp" al final:ya se ha comparado.

Quake-III-Arena

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

El valor de la celda dir[2] se deja sin marcar.

Clang

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

Al final del 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 se olvidó de la "r". en la última línea.

Motor irreal 4

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

El programador olvidó hacer 2 ediciones en la última línea. En primer lugar, “>=” debe reemplazarse por “<=; en segundo lugar, menos debe ser reemplazado por 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 la última llamada de la función qIsFinite, la variable 'h' debería haberse utilizado como argumento.

Abrir SSL

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 longitud de la cadena "BITLIST" es de 7, no de 3 caracteres.

Detengámonos aquí. Espero que los ejemplos que he demostrado sean más que suficientes.

Conclusión

De este artículo, ha aprendido que con el método Copiar-Pegar, cometer un error en el último bloque de código pegado es 4 veces más probable que en cualquier otro fragmento.

Tiene que ver con las especificidades de la psicología humana, no con las habilidades profesionales. Le mostré en este artículo que incluso los desarrolladores altamente calificados, de proyectos como Clang o Qt, tienden a cometer errores de este tipo.

Espero que mis observaciones sean útiles para los programadores, y tal vez los anime a investigar nuestra base de datos de errores. Creo que ayudará a revelar muchos patrones regulares entre los errores y elaborará nuevas recomendaciones para los programadores.

P.D. Hay errores arriba encontrados por PVS-studio.

Andréi Karpov