Ingenieria de software

Iteración: la forma más simple de repetir algo en código

iteracion
iteracion

Cuando alguien te pide que recorras una lista de usuarios, que calcules el total de una factura línea por línea o que envíes un email a cada cliente de una base de datos, estás haciendo exactamente lo mismo: iterar. Repetir un bloque de instrucciones una cantidad conocida (o determinable) de veces. Es la operación más elemental que existe en programación, y es también el primer concepto que conviene dominar antes de meterse con estructuras de datos o algoritmos.

Este artículo abre la serie Pensar como la máquina, donde construimos el terreno mental que necesitas para entender cómo la computadora ejecuta tu código. Aquí nos enfocamos en la iteración: qué es, cómo funciona por dentro, cómo se ve en memoria, y cuáles son los errores que todo programador junior comete al menos una vez.

¿Qué es iterar, en una línea?

Iterar es aplicar repetidamente un bloque de instrucciones hasta que una condición de parada se cumpla. Cada pasada completa del bloque se llama una iteración. La condición puede depender de un contador (i < 10), de un estado externo ($conexion->tieneDatos()), o de una colección que se va consumiendo (recorrer todos los elementos de un array).

Dicho en lenguaje natural:

Mientras quede trabajo por hacer, haz el trabajo una vez más.

Esa frase es la esencia de cualquier bucle. Lo que cambia entre for, while y do-while es dónde colocas la condición y cómo gestionas el contador.

Los tres bucles clásicos

Casi todos los lenguajes imperativos (PHP, Java, C, JavaScript, Python con matices) ofrecen tres estructuras para iterar. Conocer las diferencias entre ellas no es trivia: te dice qué abstracción encaja mejor con el problema.

for: cuando sabes cuántas veces

Úsalo cuando el número de iteraciones es conocido o calculable antes de empezar. Es perfecto para recorrer índices de un array, contar hasta N o ejecutar una acción una cantidad fija de veces.

// PHP
for ($i = 0; $i < 10; $i++) {
    echo "Iteración número $i\n";
}
// Java
for (int i = 0; i < 10; i++) {
    System.out.println("Iteración número " + i);
}

El for clásico tiene tres partes: inicialización ($i = 0), condición ($i < 10) y actualización ($i++). Se leen literalmente como "empieza con i en 0, sigue mientras i sea menor que 10, e incrementa i en 1 después de cada iteración".

while: cuando la condición lo dice todo

Úsalo cuando no conoces el número de iteraciones de antemano, pero sí tienes una condición clara que indica cuándo parar. Es común en loops que leen de un stream, esperan un evento o consumen una cola hasta vaciarla.

// PHP: leer líneas hasta fin de archivo
$handle = fopen("datos.txt", "r");
while (($linea = fgets($handle)) !== false) {
    procesar($linea);
}
fclose($handle);
// Java: procesar mensajes de una cola hasta que se vacíe
while (!cola.isEmpty()) {
    Mensaje m = cola.poll();
    procesar(m);
}

do-while: al menos una vez, sí o sí

Úsalo cuando quieres ejecutar el bloque al menos una vez antes de evaluar la condición. Es útil para menús interactivos, validación de entrada o reintentos.

// PHP: pedir un número válido al usuario
do {
    $entrada = readline("Ingresa un número positivo: ");
} while (!is_numeric($entrada) || (int)$entrada <= 0);
// Java: reintentar una operación hasta que funcione o se acaben los intentos
int intentos = 0;
boolean exito;
do {
    exito = llamarApi();
    intentos++;
} while (!exito && intentos < 3);

La diferencia mental es simple: while pregunta "¿debo entrar?" antes del bloque, do-while pregunta "¿debo repetir?" después. Si la condición inicial es falsa, while no ejecuta ni una vez; do-while siempre ejecuta al menos una.

Recorrer colecciones: foreach y el for-each de Java

En la práctica, muchas iteraciones son para recorrer una lista de elementos, no para contar hasta N. Para eso, PHP y Java ofrecen variantes específicas que son más claras y menos propensas a errores.

// PHP: foreach sobre array indexado
$numeros = [10, 20, 30, 40];
foreach ($numeros as $n) {
    echo $n . "\n";
}

// PHP: foreach con clave y valor en array asociativo
$precios = ["pan" => 15, "leche" => 25, "huevo" => 3];
foreach ($precios as $producto => $precio) {
    echo "$producto cuesta $precio\n";
}
// Java: for-each (enhanced for) sobre una List
List<Integer> numeros = List.of(10, 20, 30, 40);
for (int n : numeros) {
    System.out.println(n);
}

// Java: iterar sobre un Map
Map<String, Integer> precios = Map.of("pan", 15, "leche", 25);
for (Map.Entry<String, Integer> entry : precios.entrySet()) {
    System.out.println(entry.getKey() + " cuesta " + entry.getValue());
}

Estos constructos son sintactic sugar: por debajo, Java usa un Iterator y PHP expone el cursor interno del array. La ventaja es que evitas manipular índices a mano y, con ello, una clase entera de errores.

Cómo se vería en memoria

Detrás de un bucle hay muy poco: normalmente una o dos variables locales y un salto condicional. Entender esto ayuda a desmitificar los bucles y a escribir código más eficiente.

Cuando escribes for ($i = 0; $i < 1000; $i++), lo que ocurre en memoria es lo siguiente:

  1. Se reserva espacio en el stack frame de la función actual para la variable $i (típicamente 4 u 8 bytes, según el tipo). El stack es la región de memoria LIFO donde viven las variables locales.

  2. Se escribe el valor 0 en esa posición.

  3. Se evalúa la condición $i < 1000. Si es falsa, el procesador salta al final del bucle.

  4. Si es verdadera, se ejecuta el cuerpo del bucle.

  5. Se ejecuta la actualización $i++, que es una instrucción de incremento sobre la misma posición de memoria.

  6. Se salta al paso 3.

Los compiladores modernos son muy agresivos con los bucles. En Java, el JIT (Just-In-Time compiler) de la JVM suele mantener el contador i en un registro del CPU en lugar de en la memoria, eliminando accesos a RAM en cada iteración. Esto es parte de por qué un for bien escrito en Java puede ser ordenes de magnitud más rápido que iterar con llamadas a funciones.

En PHP el panorama es distinto: el intérprete trabaja con una estructura interna llamada zval que envuelve cada variable, y no hay JIT por defecto hasta PHP 8. Esto explica por qué los benchmarks de bucles puros favorecen siempre a los lenguajes compilados.

Dato importante: los bucles no acumulan stack frames. Por más largo que sea el bucle, siempre vive dentro del mismo frame de la función que lo contiene. Esta es la diferencia clave con la recursión, que veremos en el siguiente artículo y que sí apila un frame por llamada.

Operaciones básicas y su complejidad

Al pensar en iteración conviene tener en mente la complejidad temporal (cuántas operaciones realiza en función del tamaño de la entrada). La notación Big-O la veremos en detalle más adelante, pero por ahora basta con esto:

  • Un bucle que recorre una colección de n elementos una vez: O(n).

  • Un bucle dentro de otro bucle, donde ambos recorren n elementos: O(n²).

  • Un bucle que divide el rango a la mitad en cada iteración (tipo búsqueda binaria): O(log n).

Los bucles anidados son el enemigo silencioso del performance. Si tu código empieza a volverse lento cuando los datos crecen, lo primero que debes buscar son los for dentro de for.

Errores comunes

Hay un puñado de errores que todo programador junior ha cometido al menos una vez. Conocerlos de antemano te ahorra horas de depuración.

1. Off-by-one errors

El error más clásico y el más molesto. Consiste en iterar una vez de más o una vez de menos por una mala condición de parada.

// Incorrecto: se sale del array si tiene 10 elementos
$arr = [1,2,3,4,5,6,7,8,9,10];
for ($i = 0; $i <= count($arr); $i++) {
    echo $arr[$i]; // En $i=10 esto es undefined
}

// Correcto
for ($i = 0; $i < count($arr); $i++) {
    echo $arr[$i];
}

La regla mnemotécnica es: si usas <, el último índice es count - 1, que es exactamente el último válido. Si usas <=, el último índice es count, que ya es inválido.

2. Modificar la colección mientras la iteras

Eliminar o agregar elementos a una colección mientras la recorres puede causar saltos, repeticiones o excepciones. En Java, el comportamiento es directamente violento:

// ConcurrentModificationException en ejecución
List<Integer> lista = new ArrayList<>(List.of(1, 2, 3, 4));
for (int n : lista) {
    if (n % 2 == 0) {
        lista.remove(Integer.valueOf(n));
    }
}

La solución correcta es usar el Iterator explícitamente y llamar a iterator.remove(), o construir una nueva lista con los elementos que sí quieres conservar.

// Correcto con Iterator
Iterator<Integer> it = lista.iterator();
while (it.hasNext()) {
    if (it.next() % 2 == 0) {
        it.remove();
    }
}

En PHP el comportamiento es menos explosivo pero igual de traicionero: foreach internamente trabaja sobre una copia del array (en la mayoría de los casos), así que modificarlo suele no tener el efecto esperado.

3. Bucles anidados sin control de complejidad

Este no es un error de sintaxis, es un error de diseño, y es probablemente el más costoso:

// Encontrar duplicados: O(n²)
for (int i = 0; i < arr.length; i++) {
    for (int j = i + 1; j < arr.length; j++) {
        if (arr[i] == arr[j]) {
            duplicados.add(arr[i]);
        }
    }
}

Con 1,000 elementos esto hace medio millón de comparaciones. Con 10,000, son 50 millones. En producción, con datasets reales, es suficiente para tirar el servidor. La alternativa casi siempre pasa por usar una tabla hash para lograr O(n) —pero eso es tema de la Serie 2.

4. Loops infinitos accidentales

El clásico: olvidar actualizar la variable de parada dentro del cuerpo del bucle.

$i = 0;
while ($i < 10) {
    echo $i;
    // Se nos olvidó incrementar $i: este bucle nunca termina
}

Con for, es más difícil caer porque la actualización está en la propia cabecera del bucle. Por eso, cuando la condición de parada depende de más de un contador, prefiere for sobre while siempre que puedas.

Cuándo iterar y cuándo no

La iteración es la herramienta por defecto para repetir trabajo, pero no siempre es la mejor opción. Hay dos alternativas principales a tener en mente:

  • Recursión: útil cuando el problema se define naturalmente en términos de sí mismo (árboles, grafos, divide y vencerás). La veremos en el siguiente artículo.

  • Operaciones vectorizadas o funcionales: en PHP, funciones como array_map, array_filter y array_reduce; en Java, los Streams. No son estrictamente más rápidas, pero el código es más declarativo y menos propenso a errores de contadores.

// En lugar de iterar a mano para transformar
$duplicados = array_map(fn($n) => $n * 2, [1, 2, 3, 4]);
// Streams en Java
List<Integer> duplicados = List.of(1, 2, 3, 4).stream()
    .map(n -> n * 2)
    .toList();

Internamente estos constructos iteran, pero te abstraen de los detalles. Para código de negocio los prefiero casi siempre. Para código crítico de performance, el for clásico sigue ganando.

Resumen

La iteración es repetir un bloque de instrucciones hasta que una condición se cumpla. Los tres bucles clásicos son for (cuando conoces el número de repeticiones), while (cuando la condición lo dicta) y do-while (cuando necesitas ejecutar al menos una vez). foreach y el for-each de Java son azúcar sintáctico para recorrer colecciones sin manipular índices.

Por dentro, un bucle son dos o tres variables locales en el stack y un salto condicional. No apila frames, no consume memoria adicional por iteración, y los compiladores modernos lo optimizan agresivamente.

Los errores más comunes son off-by-one, modificar la colección durante la iteración, bucles anidados sin control de complejidad y loops infinitos por mala actualización del contador.

En el siguiente artículo de esta serie veremos la otra gran herramienta para repetir trabajo: la recursión. Es más elegante para ciertos problemas, pero paga un precio en memoria que conviene entender antes de abusar de ella.

Comentarios (0)

Sé el primero en comentar.

Deja un comentario

Protegido con reCAPTCHA — Privacidad · Términos

Historias relacionadas