Callbacks en Rust: del dolor a la elegancia a través de modismos

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

Introducción: por qué los callbacks en Rust son un desafío (y una oportunidad)

\n

Si alguna vez has escrito en JavaScript, Python o C#, los callbacks son para ti una herramienta familiar. Pasas una función que será llamada más tarde y vives tranquilo. Pero cuando llegas a Rust, este patrón simple de repente se convierte en un campo minado: propiedad, préstamo, tiempos de vida, dyn vs impl...

\n

En Stack Overflow, la pregunta "Idiomatic callbacks in Rust" ha acumulado miles de visitas. Esto significa que incluso desarrolladores experimentados tropiezan con este tema. Pero la buena noticia es que Rust ofrece no una, sino varias formas elegantes de implementar callbacks. Y hoy analizaremos todas ellas, desde closures simples hasta poderosos trait objects y patrones enum.

\n

Este artículo es para ti si:

\n
    \n
  • Has comenzado a estudiar Rust y quieres entender cómo organizar correctamente arquitecturas asíncronas y basadas en eventos;
  • \n
  • Estás migrando desde otros lenguajes y buscas análogos idiomáticos para patrones familiares;
  • \n
  • Quieres escribir código limpio, seguro y eficiente en Rust.
  • \n
\n

¡Allá vamos!

\n\n

El problema: ¿qué tienen de malo los callbacks simples?

\n

Imagina que escribimos un planificador de tareas simple. En Python se vería así:

\n
def schedule(callback):\n    # hacemos algo\n    callback(result)
\n

En Rust, el primer intento podría ser:

\n
fn schedule(callback: fn(i32) -> i32) {\n    let result = 42;\n    callback(result);\n}
\n

¡Y esto funciona! Pero solo para funciones, no para closures. Intenta pasar un closure que capture una variable:

\n
let factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // ¡Error! expected fn pointer, found closure
\n

El compilador se queja con razón: fn es un puntero a función, mientras que un closure es un tipo anónimo que puede capturar el entorno. Necesitamos un enfoque más flexible.

\n\n

Enfoque 1: Closures con impl Fn (simplicidad y flexibilidad)

\n

La forma más simple y más utilizada es usar un tipo genérico con el trait Fn, FnMut o FnOnce.

\n

¿Cuándo usar Fn, FnMut, FnOnce?

\n
    \n
  • FnOnce — si el callback se llama exactamente una vez (consume los valores capturados);
  • \n
  • FnMut — si el callback puede modificar el estado capturado (se llama varias veces);
  • \n
  • Fn — si el callback no modifica el estado (función pura).
  • \n
\n

Ejemplo con impl Fn:

\n
fn schedule<F>(callback: F)\nwhere\n    F: Fn(i32) -> i32,\n{\n    let result = 42;\n    println!("Resultado: {}", callback(result));\n}\n\nfn main() {\n    let factor = 2;\n    let closure = |x| x * factor;\n    schedule(closure); // ¡Funciona!\n    \n    // También se puede pasar una función normal\n    fn double(x: i32) -> i32 { x * 2 }\n    schedule(double);\n}
\n

Ventajas: máxima flexibilidad, soporta closures, seguro, legible.
\nDesventajas: monomorfización — para cada tipo de closure, el compilador genera una versión separada de la función. Si hay muchos callbacks, el código puede inflarse.

\n\n

Enfoque 2: Trait objects Box<dyn Fn> (despacho dinámico)

\n

Si quieres almacenar callbacks en colecciones (por ejemplo, en un Vec) o pasarlos entre hilos, los genéricos no funcionarán — necesitas un solo tipo. Aquí es donde los trait objects vienen al rescate:

\n
type Callback = Box<dyn Fn(i32) -> i32>;\n\nstruct Scheduler {\n    callbacks: Vec<Callback>,\n}\n\nimpl Scheduler {\n    fn new() -> Self {\n        Scheduler { callbacks: Vec::new() }\n    }\n    \n    fn register(&mut self, callback                

Blogs

Book Recommendations