Introducción: por qué los callbacks en Rust son un desafío (y una oportunidad)
\nSi 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...
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.
\nEste 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
¡Allá vamos!
\n\nEl problema: ¿qué tienen de malo los callbacks simples?
\nImagina que escribimos un planificador de tareas simple. En Python se vería así:
\ndef schedule(callback):\n # hacemos algo\n callback(result)\nEn Rust, el primer intento podría ser:
\nfn 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:
\nlet factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // ¡Error! expected fn pointer, found closure\nEl 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.
Enfoque 1: Closures con impl Fn (simplicidad y flexibilidad)
\nLa forma más simple y más utilizada es usar un tipo genérico con el trait Fn, FnMut o FnOnce.
¿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
Ejemplo con impl Fn:
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}\nVentajas: 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.
Enfoque 2: Trait objects Box<dyn Fn> (despacho dinámico)
\nSi 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:
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