Generadores síncronos: eficiencia con sync* y yield

Las funciones generadoras síncronas, marcadas con sync*, son un mecanismo para producir una secuencia de valores de forma lazy (perezosa). En lugar de calcular todos los elementos de una colección y guardarlos en memoria (como un List), la función se pausa en cada instrucción yield, devolviendo un elemento y manteniendo su estado interno intacto para la próxima petición. Por diseño, el tipo de retorno de estas funciones siempre es Iterable<T>.

Esto funciona porque el Dart VM transforma la función en una máquina de estados. Cuando un consumidor solicita el siguiente elemento (mediante el método moveNext() del iterador), la ejecución de la función avanza hasta encontrar el siguiente yield, donde se detiene y entrega el valor. Esta arquitectura permite procesar grandes volúmenes de datos sin necesidad de cargar todo el conjunto en la heap. Debes usar este patrón cuando trabajes con secuencias de datos potencialmente infinitas, recorridos de estructuras de datos complejas (como árboles) o cuando quieras realizar transformaciones de datos sin el coste de materializar colecciones intermedias. Si intentas usar yield en una función que no sea sync*, el compilador te dará un error de sintaxis; y si intentas consumir un generador infinito usando un método que requiere conocer el tamaño total del iterable (como .toList()), bloquearás el hilo de ejecución o agotarás la memoria.

class Carpeta {
  final String nombre;
  final List<String> archivos;
  final List<Carpeta> subcarpetas;

  Carpeta(this.nombre, {this.archivos = const [], this.subcarpetas = const []});

  /// Generador que recorre la jerarquía de archivos de forma perezosa.
  Iterable<String> listarTodo() sync* {
    // Iteramos sobre los archivos de la carpeta actual.
    for (var archivo in archivos) {
      yield archivo; // Produce un elemento y pausa la ejecución.
    }

    // Delegamos la iteración a los generadores de las subcarpetas.
    for (var subcarpeta in subcarpetas) {
      yield* subcarpeta.listarTodo(); // yield* delega en otro Iterable.
    }
  }
}

void main() {
  final sistema = Carpeta('root',
    archivos: ['config.yaml', 'README.md'],
    subcarpetas: [
      Carpeta('src', archivos: ['main.dart', 'utils.dart']),
      Carpeta('lib', subcarpetas: [
        Carpeta('core', archivos: ['engine.dart']),
        Carpeta('api', archivos: ['client.dart']),
      ]),
    ],
  );

  print('Iniciando escaneo de archivos...');

  // El cuerpo de listarTodo() no se ejecuta hasta que el 'for' pide el primer elemento.
  for (final archivo in sistema.listarTodo()) {
    print('Encontrado: $archivo');
  }

  print('Escaneo completado.');
}

En el ejemplo anterior, fíjate en cómo listarTodo() devuelve un Iterable<String>. Cuando llamamos a sistema.listarTodo() en el main, no se está ejecutando la lógica de recorrido todavía; solo se ha creado el iterador. La ejecución real comienza cuando el bucle for solicita el primer elemento.

La clave de la eficiencia aquí es la diferencia entre yield y yield*. Cuando usamos yield archivo, estamos entregando un valor individual directamente al consumidor. Sin embargo, al llegar a yield* subcarpeta.listarTodo(), estamos utilizando delegación de iteración. En lugar de crear una lista intermedia con los archivos de la subcarpeta, yield* le dice al iterador actual: “toma este otro iterable y entrega sus elementos como si fueran míos”. Esto permite que la recursividad sea extremadamente eficiente en términos de memoria, ya que solo mantenemos en el stack la ruta de llamadas necesaria para llegar al nodo actual.

El error frecuente

Un error muy común es intentar convertir un generador en una lista de forma ambiciosa, ignorando la naturaleza lazy del objeto. Si tienes un generador que produce una secuencia infinita (por ejemplo, una función que genera todos los números pares), esto es perfectamente válido:

Iterable<int> numerosPares() sync* {
  var i = 0;
  while (true) {
    yield i;
    i += 2;
  }
}

void errorFatal() {
  // Esto causará un bucle infinito y eventualmente un OutOfMemory error.
  final lista = numerosPares().toList(); 
  print(lista);
}

El problema es que .toList() es un método eager (ansioso): intenta consumir todo el iterable antes de devolver el resultado. Como numerosPares nunca termina, el programa nunca saldrá del método .toList(). Cuando trabajes con generadores, recuerda que el consumidor debe tener un criterio de parada o trabajar mediante el procesamiento de un elemento a la vez.

27

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio