Iterable<T> es la interfaz base que define una secuencia de elementos que pueden ser recorridos uno a uno. A diferencia de una List, no es una estructura de datos que almacene elementos en posiciones de memoria específicas; es un contrato que garantiza que puedes pedir el “siguiente” elemento mediante un objeto Iterator. Esta distinción es fundamental: mientras que una List es una colección estática en memoria, un Iterable es un plan de navegación.
Este diseño permite la evaluación perezosa (lazy evaluation), un mecanismo donde el cálculo de un elemento solo ocurre en el preciso momento en que se solicita. Esto es posible gracias al protocolo de iteración, donde el Iterable delega la responsabilidad del estado al Iterator. Este último utiliza el método moveNext() para avanzar el puntero y la propiedad current para exponer el valor actual. Gracias a esto, puedes trabajar con secuencias infinitas, ya que nunca se intentará calcular el “final” de la secuencia, sino solo los elementos que decidas consumir.
Debes usar Iterable cuando necesites encadenar transformaciones como map, where o take para procesar flujos de datos de forma eficiente sin crear colecciones intermedias que saturen la memoria. Sin embargo, si intentas realizar operaciones de acceso aleatorio (como iterable[5]) en un iterable que no sea una List, o si iteras múltiples veces un iterable que depende de un generador con efectos secundarios, el rendimiento caerá drásticamente o obtendrás resultados inconsistentes.
import 'dart:math';
/// Una secuencia infinita de números de Fibonacci.
/// Al implementar Iterable, definimos cómo se generan los datos, no cómo se guardan.
class Fibonacci extends Iterable<int> {
@override
Iterator<int> get iterator => _FibonacciIterator();
}
class _FibonacciIterator implements Iterator<int> {
int _a = 0;
int _b = 1;
int _current = 0;
bool _isFirst = true;
@override
bool get hasNext => true; // La secuencia es infinita
@override
int get current => _current;
@override
bool moveNext() {
_current = _a;
int next = _a + _b;
_a = _b;
_b = next;
return true;
}
}
void main() {
// 1. Uso de un generador de fábrica (Iterable.generate)
// Crea un iterable de números aleatorios de 0 a 9.
final randomNumbers = Iterable.generate(10, (i) => Random().nextInt(100));
// 2. Transformaciones perezosas (Lazy)
// Fibonacci es infinita, pero 'take' limita la ejecución.
// 'where' y 'map' no ejecutan nada hasta que llamamos a 'toList()'.
final processedFibonacci = Fibonacci()
.take(10) // Limitamos a 10 elementos para evitar un bucle infinito
.where((n) => n % 2 == 0) // Solo pares
.map((n) => 'Fib: $n'); // Transformamos a String
print('--- Fibonacci Procesado ---');
print(processedFibonacci.toList());
print('\n--- Random Numbers (Original) ---');
// Aquí se materializa el iterable en una lista real en memoria
final listified = randomNumbers.toList();
print(listified);
// 3. Diferencia crucial: Iterable vs List
// Un Iterable no tiene un 'length' garantizado sin recorrerlo (excepto en List)
// pero podemos usarlo para inspeccionar elementos.
print('\nPrimeros 3 números aleatorios: ${randomNumbers.take(3).toList()}');
}
Análisis del código
En la clase Fibonacci, hemos implementado el protocolo de iteración manualmente. Al extender Iterable, nos vemos obligados a proporcionar un Iterator. El _FibonacciIterator mantiene el estado de la secuencia (_a y _b) de forma interna. Fíjate que hasNext siempre devuelve true: esto es legal en un Iterable y es lo que permite crear secuencias infinitas.
Cuando en el main ejecutamos Fibonacci().take(10).where(...).map(...), no se ha realizado ningún cálculo matemático. Dart ha creado una cadena de objetos “envoltorios”. take(10) es el único que tiene el control de detener la ejecución. Solo cuando llamamos a toList(), se inicia el proceso: se pide un iterator, se llama a moveNext(), el valor pasa por el filtro de where, luego por el map y finalmente se guarda en la nueva List.
La función Iterable.generate es un método de utilidad que nos ahorra implementar todo el protocolo manualmente cuando solo necesitamos una secuencia basada en un índice. Sin embargo, el resultado de generate sigue siendo un Iterable, no una List, lo que significa que el cálculo de Random().nextInt se pospone hasta que la iteración ocurre.
El error frecuente
Un error común al trabajar con transformaciones es asumir que un Iterable es una estructura de datos estática. Si la lógica de transformación tiene efectos secundarios (como modificar una variable externa o realizar una llamada a una API), iterar el mismo Iterable dos veces producirá resultados diferentes o comportamientos erráticos.
int contador = 0;
void main() {
// Generamos un iterable que incrementa un contador cada vez que se accede a un elemento
final iterableConEfecto = List.generate(3, (i) {
contador++;
return i;
});
print('Primera iteración: ${iterableConEfecto.toList()} | Contador: $contador');
// El contador es 3
print('Segunda iteración: ${iterableConEfecto.toList()} | Contador: $contador');
// El contador es 6, ¡pero el contenido es el mismo!
// El peligro real ocurre cuando la lógica de transformación NO es pura:
var transformacionPeligrosa = iterableConEfecto.map((e) {
contador++;
return e * 2;
});
print('Transformación 1: ${transformacionPeligrosa.toList()} | Contador: $contador');
print('Transformación 2: ${transformacionPeligrosa.toList()} | Contador: $contador');
// El contador saltará descontroladamente cada vez que intentes leer transformacionPeligrosa.
}
Dominar la evaluación perezosa es la diferencia entre un proceso que consume toda la RAM y uno que procesa gigabytes de datos con un uso de memoria constante.
N° 37