Fundamentos

Errores típicos en estructuras de control

Básicos Fundamentos

Todo programador ha cometido alguno de estos errores típicos en estructuras de control cuando comenzaba a programar. Especialmente con estructuras de control iterativas, en las que es muy fácil cometer errores si no sabemos bien lo que estamos haciendo.

El temido bucle infinito

Imaginemos que montamos un bucle WHILE con el que, de nuevo, contamos los primeros 1000 números. Pero, a diferencia de en ocasiones anteriores, un programador aprendiz muestra lo siguiente:

sumatorio = 0
incremento = 1
WHILE (incremento !== 1001) {
  sumatorio += incremento
  incremento += 1
}
IMPRIMIR sumatorio

A diferencia del ejemplo que hemos utilizado anteriormente, ahora la condición es que la variable incremento sea distinta a 1001 en lugar de conforme estábamos haciéndolo hasta ahora, que era mientras incremento sea menor o igual a 1000. El programa de nuestro aprendiz funciona perfectamente bien e igual al nuestro, pues la condición se cumple para los 1000 primeros valores que coge incremento. Como hemos dicho anteriormente, un mismo programa puede escribirse de muchas formas distintas.

Cambiando las condiciones del problema

Pero ahora le proponemos cambiar el problema y le decimos que calcule únicamente la suma de los 1000 primeros números pares. Cogiendo su programa anterior, nuestro aprendiz hace el siguiente cambio:

sumatorio = 0
incremento = 2
WHILE (incremento !== 1001) {
  sumatorio += incremento
  incremento += 2
}
IMPRIMIR sumatorio

Orgulloso, nos entrega el nuevo código. Únicamente ha cambiado el valor inicial de incremento, que ahora vale 2 (bien hecho, no queremos que sume el 1), y el valor de incremento dentro del bucle se va aumentando de 2 en 2 (bien, pues sólo queremos sumar los pares). Pero cuando ejecutamos este código vemos que algo va mal.

¿Qué está ocurriendo? El programa ha entrado en un bucle infinito y la ejecución del programa nunca acaba. Nunca se nos muestra por pantalla cuánto vale sumatorio y vemos que el programa sigue eternamente en ejecución.

Una pequeña traza

Vayamos paso a paso para ver qué está ocurriendo. La clave está en la condición. Nuestro programa comienza sumando el 2, luego a este 2 le sumará el 4, luego le sumará el 6, luego el 8, etc. Parece que todo va bien.

Avancemos un poco y lleguemos al 996, por ejemplo. Nuestro programa sigue sumando este valor al total del sumatorio, pasa luego al 998, que también suma, llega al 1000, realiza la suma y cuando llega al 1002… el programa no se detiene y sigue sumando el 1002, luego el 1004, el 1006, el 1008 y así hasta nunca terminar. ¿Por qué?

Debido a la condición. Fijémonos en que la condición del WHILE es que se siga ejecutando mientras el valor de incremento sea distinto de 1001. Cuando el programa sumaba de uno en uno, por lógica iba a llegar a ese valor de 1001 en algún momento e iba a parar. Pero ahora, incremento nunca tendrá el valor 1001, pues después de llegar al valor 1000 pasará al valor 1002. La condición del WHILE siempre será cierta y nuestro programa seguirá eternamente ejecutándose.

Curiosidad
En algunas ocasiones, cuando se nos cuelga un ordenador y no responde a nada de lo que hagamos, muchas veces es porque ha habido algún tipo de error y la ejecución del sistema operativo ha entrado en un bucle infinito.

Por ello, debemos tener especial cuidado al trabajar con bucles y asegurarnos siempre de que el bucle vaya a terminar en algún momento.

Ejemplos de bucles infinitos

Vamos a ver unos cuantos fragmentos de pseudocódigo que llegan a bucles infinitos. Algunos nos parecerán muy ilógicos, pero ocurrir pueden ocurrir.

FOR (indice = 10; indice > 3; indice += 1) {
  IMPRIMIR indice
}

Este programa nunca acabará, pues indice empieza valiendo 10, en la siguiente iteración valdrá 11, más tarde será un 12, luego un 13, 14, 15… Es decir, siempre cumplirá la condición de que sea mayor que 3, por tanto entra un bucle infinito.

indice_2 = 0;
FOR (indice_1 = 0; indice_1 < 3; indice_2 += 1) {
  IMPRIMIR indice_1
}

En este programa, que tampoco acabaría nunca, nos hemos equivocado y hemos utilizado dos índices distintos. El que se va incrementando es indice_2 pero la condición se expresa con el indice_1, el cual no se modifica en todo el bucle, siempre la cumplirá y, por tanto, entra en bucle infinito.

FOR (indice = 0; indice < 3; indice += 1) {
  IMPRIMIR indice
  indice -= 1
}

Tampoco acaba nunca este programa, pues estamos modificando la variable indice dentro del mismo bucle de manera que, si el bucle le aumenta la unidad una vez, dentro se la restamos también una vez, con lo que su valor nunca cambiará y entraremos en un bucle infinito.

WHILE (VERDADERO) {
  IMPRIMIR 'Estoy en un bucle infinito'
}

El ejemplo por excelencia. Un bucle WHILE en cuya condición le ponemos el valor booleano verdadero. Obviamente el programa nunca acabará y estaremos en un bucle infinito.

Cuidar la condición de nuestros bucles

Así pues, para evitar los errores típicos en estructuras de control debemos ser muy cuidadosos con la condición de finalización de nuestros bucles. Hay que tener muy claro qué vamos a hacer con el índice utilizado por el bucle o por la condición para que nunca entre en un bucle infinito.

Errores al recorrer arrays

Es bastante común el siguiente error cuando estamos recorriendo un array:

frutas = ['manzana', 'plátano', 'pera', 'cereza', 'melocotón']
FOR (indice = 0; indice <= 5; indice += 1) {
  IMPRIMIR elemento_indice_de_variable_frutas
}

Nuestro array frutas contiene 5 elementos y, para recorrerlos, creamos un FOR hasta que indice sea menor o igual a 5 (es decir, mayor que 5). Nuestro aprendiz cree que tiene el programa perfecto. Sin embargo, al ejecutarlo le da error. ¿Qué ha ocurrido?

Sigamos los pasos. En primer lugar, indice vale 0 y accedemos al primer valor de frutas para imprimir ‘manzana’. Posteriormente, indice vale 1 e imprime ‘plátano’. Seguiríamos con indice 2 y salida ‘pera’, indice 3 mostrando ‘cereza’, indice 4 imprimiendo ‘melocotón’ y… al llegar al indice 5 la condición del FOR sigue cumpliéndose pero no existe el elemento de índice 5 en nuestro array frutas.

Éste es un error común al no estar acostumbrados a empezar a contar desde 0. Si hubiéramos puesto que indice valiera 1, no hubiéramos accedido jamás al elemento ‘manzana’ que tiene índice 0, y nos hubiera dado igualmente error al intentar acceder al índice 5 de frutas porque sigue sin existir.

La solución pasa por cambiar la condición de menor o igual a sólo menor. Suele ocurrir más de lo que podemos pensarnos, pues pasarnos de 1 cuando contamos en un array es frecuente ya que el índice numérico acaba en un valor menos que el total de elementos al iniciar la cuenta en 0. Es otro de los errores típicos en estructuras de control.

Solución sencilla
Si tenemos la opción de utilizar un bucle FOREACH, la solución sería más sencilla, pues lo podemos usar y él se encargaría de recorrer el array desde el primer al último elemento. De esta forma, nos despreocupamos de saber en qué índice empieza el array y en cuál termina.

Utilizar el mismo índice en dos bucles anidados

Otro de los errores típicos en estructuras de control es la utilización de dos índices idénticos cuando estamos utilizando nuestro bucle FOR. Es una costumbre que cogen casi todos los programadores el hecho de utilizar el nombre de variable i cuando se va a recorrer un bucle.

FOR (i = 0; i < 5; i += 1) {
  IMPRIMIR i
}

Hasta aquí, todo bien. Sin embargo, es un error muy común que, cuando se anidan dos bucles, tengamos tendencia a seguir llamando i al índice del segundo bucle. He aquí un ejemplo de código erróneo.

FOR (i = 0; i < 5; i += 1) {
  FOR (i = 0; i < 3; i += 1) {
    IMPRIMIR i
  }
}

Este código entraría en un bucle infinito. Hagamos una pequeña traza. Inicialmente, en el primer paso del primer bucle asignamos el valor 0 a i. Luego entramos en el segundo bucle y volvemos a asignar el valor 0 a i imprimiendo un 0 por pantalla. Seguimos en este segundo bucle e imprimimos un 1 y luego un 2. Cuando i vale 3, el segundo bucle finaliza y llegamos a la segunda iteración del primer bucle.

En ella, volvemos a sumar uno al valor de i, el cual recordemos que era 3. Por tanto, ahora tenemos i con valor 4 y entramos, de nuevo, en el segundo bucle. En él, volvemos a poner i a 0… y aquí repetimos lo mismo. Imprimirá un 0, luego un 1 y luego un 2, volverá al anterior con valor 3, subirá al valor 4 y volveremos al segundo empezando desde 0. El programa no acabaría nunca.

Si se quieren recorrer dos bucles, o más, hay que utilizar índices distintos. Éste sí sería un buen programa.

FOR (i = 0; i < 5; i += 1) {
  FOR (j = 0; j < 3; j += 1) {
    IMPRIMIR i
    IMPRIMIR j
  }
}

Por pantalla veríamos 000102101112202122303132404142, es decir, primero i y j serían 0 y 0, luego serían 0 y 1, luego 0 y 2, luego 1 y 0, luego 1 y 1… hasta llegar a sus últimos valores, 4 y 2. Como veis, son los números que nos ha impreso por pantalla. Ésta es la forma en la que se suelen recorrer las matrices.

La sentencia break

¿Podemos “romper” el bucle de alguna otra forma que no sea con la condición? Sí, mediante la sentencia break que incorporan la mayoría de lenguajes de programación. Veamos un ejemplo un poco ilógico utilizando el primer programa fallido de nuestro aprendiz:

sumatorio = 0
incremento = 2
WHILE (incremento !== 1001) {
  sumatorio += incremento
  incremento += 2
  IF (incremento > 1000) {
    BREAK;
  }
}
IMPRIMIR sumatorio

Este programa funciona. Básicamente le estamos diciendo en el IF que, si la variable incremento es mayor de 1000 en ese punto de la ejecución, “rompa” el bucle y salga. De esta forma, cuando la variable llegue al valor 1002 que no detenía el WHILE, sí entrará por el IF y ejecutará el break. Este ejemplo, como hemos dicho, es un poco ilógico, pues no necesitamos el IF ya que lo que tenemos que hacer es mejorar la condición del WHILE. Pero nos sirve para ilustrar el uso de break.

Un uso más razonable para break

Por la razón anterior, vamos a definir un ejemplo más claro de cómo usar break. Tenemos un array que contiene el censo total de España, más de 40 millones de personas. Y pretendemos buscar una de ellas. Un primer programa, que funciona además, es el siguiente:

// El siguiente array contiene más de 40 millones de personas.
// Lo hemos expresado con ... para no haceros leerlos todos
personas = ['José Suárez', 'María García', 'Marcos López', 'Susana Costa', ..., 'Jesús Pérez']

persona_a_buscar = 'Marcos López'
FOREACH (personas AS persona) {
  IF (persona == persona_a_buscar) {
    IMPRIMIR 'Está persona está censada en España'
  }
}

El programa es funcional. Tenemos nuestro array de más de 40 millones de elementos que recorremos con el FOREACH y queremos ver si existe ‘Marcos López’. La primera persona que tenemos, ‘José Suárez’, no coincide con la buscada, así que la condición del IF no se cumple. La segunda persona que tenemos, ‘María García’, tampoco coincide. Seguimos.

A continuación, la tercera persona, ‘Marcos López’, es justo la que estábamos buscando. El IF se cumple e imprimimos el mensaje por pantalla. ¿Qué ocurre a continuación? Para nosotros la búsqueda ha finalizado, pero el FOREACH sigue su ejecución con la cuarta persona, la quinta, la sexta… y así hasta comprobar las más de 40 millones de personas.

Ésto es un malgasto de tiempo y recursos de nuestro ordenador enorme. No se trata de uno de los errores típicos en estructuras de control, pero tenemos que ser, siempre, lo más eficientes posibles, así pues podemos considerar que a nivel de programación no está bien. Y si, al igual que en el “mundo real”, en el momento en que hemos encontrado lo que buscábamos dejamos de buscar, el programa debe hacer lo mismo. Ahí entra la sentencia break.

personas = ['José Suárez', 'María García', 'Marcos López', 'Susana Costa', ..., 'Jesús Pérez']

persona_a_buscar = 'Marcos López'
FOREACH (personas AS persona) {
  IF (persona == persona_a_buscar) {
    IMPRIMIR 'Está persona está censada en España'
    BREAK
  }
}

Ahora sí, nuestro programa, una vez encuentra la persona buscada, imprime el mensaje por pantalla y ejecuta el break, saliendo del bucle en el tercer paso (o en el paso que sea cuando encuentre lo que busca).

A la gente no le gusta el break

Es cierto. Hay muchos programadores que reniegan del break y creen que es mejor encontrar la condición correcta. Por ejemplo, el programa anterior se puede escribir de esta forma:

personas = ['José Suárez', 'María García', 'Marcos López', 'Susana Costa', ..., 'Jesús Pérez']

persona_a_buscar = 'Marcos López'

indice = 0
encontrado = FALSO
WHILE (encontrado == FALSO AND indice < 40000000) {
  IF (elemento_indice_en_personas == persona_a_buscar) {
    IMPRIMIR 'Está persona está censada en España'
    encontrado = VERDADERO
  }
  indice += 1
}

Analicemos bien el programa. Ahora utilizamos un bucle WHILE, por lo que inicializamos antes una variable indice. De igual manera, inicializamos una variable encontrado con valor falso. La condición del WHILE será “mientras no se haya encontrado y el índice sea menor a 40 millones (por redondear), haz…”. La primera vez, encontrado siendo falso e indice siendo 0, la condición se cumple y entramos.

Una vez dentro, evaluamos con el IF si la persona actual (a la que accedemos por el índice en el que estamos) es igual a la persona buscada. Si no lo es, incrementamos indice y volvemos a la condición del WHILE. Si lo es, imprimimos el mensaje por pantalla y cambiamos el valor de encontrado a verdadero.

El WHILE se detendrá en dos posibles ocasiones. Cuando indice sea igual o mayor a 40 millones y, por tanto, hayamos recorrido todo el array, se detendrá la ejecución del bucle y así no entramos en bucle infinito. Pero cuando sí hemos encontrado la persona buscada, al cambiar ahora el valor de encontrado, el bucle WHILE no cumple la primera condición y también se detiene.

Es una forma de no utilizar break. ¿Es mejor? Ciertamente no lo sé. Para algunos puristas es más legible no utilizar break, pero para otros es una opción más a tener en cuenta. Al final, hacedlo como mejor os guste o sepáis.

Resumen

Hemos repasado algunos de los errores típicos en estructuras de control, especialmente en la utilización de bucles. Y definido, de paso, la instrucción break. Pese a todo, estamos seguros de que, en algún momento, ya seáis aprendices o veteranos, entraréis en el temido bucle infinito.

Definidas, por tanto, las estructuras de control más utilizadas, podemos pasar ya en el próximo artículo a ver el concepto de funciones.

Deja un comentario