Devolviendo iteradores en Rust: secretos de rendimiento y abstracciones de costo cero

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Por qué devolver un iterador es un dolor

\n\n

¿Alguna vez ha escrito una función en Rust que deba devolver una colección procesada a través de una cadena de iteradores? Si es así, seguramente se ha enfrentado a la pregunta: "¿Qué tipo devuelvo?" Parece lógico escribir fn get_data() -> Iterator, pero el compilador inmediatamente muestra un error: "the trait `Iterator` cannot be made into an object".

\n\n

Este problema es tan común que en Stack Overflow la pregunta "What is the correct way to return an Iterator (or any other trait)?" ha acumulado miles de visitas. Y no es de extrañar: la elección correcta afecta no solo la comodidad del código, sino también el rendimiento. En este artículo analizaremos todas las formas de devolver iteradores en Rust, sus ventajas y desventajas, y, lo más importante, cómo no perder rendimiento en algo tan simple.

\n\n

El artículo está escrito para pythonlib.ru — un sitio educativo donde no tememos mirar bajo el capó de los lenguajes populares. Rust, con sus abstracciones de costo cero, nos brinda la oportunidad única de escribir código que es a la vez elegante y rápido. Pero solo si sabemos cómo usar sus herramientas correctamente.

\n\n

El problema: los traits no tienen tamaño

\n\n

En Rust, los traits son interfaces, pero a diferencia de Java o Go, no son tipos con un tamaño conocido. El compilador necesita saber cuánta memoria ocupa el valor devuelto para asignar espacio en la pila. Y el tamaño de una implementación concreta de un iterador (por ejemplo, std::iter::Map o std::iter::Filter) puede ser diferente.

\n\n

Por lo tanto, no se puede devolver directamente un Iterator. Hay que elegir uno de los siguientes patrones:

\n\n
    \n
  • Despacho estático — impl Iterator (devuelve un tipo concreto)
  • \n
  • Despacho dinámico — Box<dyn Iterator> (el tipo se borra)
  • \n
  • Enum dispatch — un enum propio para todos los iteradores posibles
  • \n
  • Genéricos — una función que acepta y devuelve tipos genéricos
  • \n
\n\n

Cada enfoque tiene su costo. Analicémoslos con ejemplos reales.

\n\n

Método 1: impl Iterator — el estándar de oro

\n\n

La forma más simple y eficiente es usar impl Trait en el tipo de retorno. Es azúcar sintáctico que le dice al compilador: "voy a devolver algún tipo concreto que implementa el trait, pero tú mismo lo deduces".

\n\n
fn get_numbers() -> impl Iterator<Item = i32> {\n    (0..10).map(|x| x * 2)\n}\n\nfn main() {\n    for num in get_numbers() {\n        println!("{}", num);\n    }\n}
\n\n

Ventajas:

\n
    \n
  • Máximo rendimiento — despacho estático, sin llamadas virtuales
  • \n
  • El compilador puede inline el código y aplicar optimizaciones
  • \n
  • Tamaño mínimo del binario resultante
  • \n
\n\n

Desventajas:

\n
    \n
  • El tipo devuelto debe ser único — no se puede devolver Map en un caso y Filter en otro
  • \n
  • El tipo puede ser muy complejo (por ejemplo, Map<Filter<...>>), lo que dificulta la lectura de los errores del compilador
  • \n
  • No se puede almacenar en una estructura o vector — solo en funciones
  • \n
\n\n

Este método es ideal para el 90% de los casos. Si su función siempre devuelve la misma cadena de iteradores, use impl Iterator sin dudarlo.

\n\n

Método 2: Box<dyn Iterator> — flexibilidad a costa de rendimiento

\n\n

Cuando necesita devolver diferentes tipos de iteradores según condiciones (por ejemplo, al analizar argumentos de línea de comandos), el despacho dinámico a través de Box<dyn Iterator> viene al rescate.

\n\n
fn get_data(use_filter: bool) -> Box<dyn Iterator<Item = i32>> {\n    let data = vec![1, 2, 3, 4, 5];\n    if use_filter {\n                

Blogs

Book Recommendations