Las closures de Rust son funciones anónimas que pueden capturar variables del entorno donde se definen. El compilador infiere automáticamente si la closure captura por referencia, por referencia mutable o por valor, y asigna uno de los tres traits: Fn, FnMut o FnOnce. Entender la diferencia entre ellos es clave para escribir código correcto y eficiente.
FnOnce, FnMut y Fn: la jerarquía de captura
Los tres traits forman una jerarquía de restricciones: FnOnce es el más general (solo puede llamarse una vez porque mueve valores del entorno), FnMut puede llamarse varias veces mutando el entorno, y Fn es el más restrictivo (puede llamarse cualquier número de veces sin mutar nada).
fn aplicar_una_vez<F: FnOnce() -> String>(f: F) -> String {
f() // f se consume aquí; no puede volver a llamarse
}
fn aplicar_veces<F: Fn(u32) -> u32>(f: F, n: u32) -> Vec<u32> {
(0..n).map(|i| f(i)).collect()
}
fn acumular<F: FnMut(u32) -> u32>(mut f: F, valores: &[u32]) -> Vec<u32> {
valores.iter().map(|&v| f(v)).collect()
}
fn main() {
let nombre = String::from("Rust");
// FnOnce: mueve `nombre` al llamarse
let saludo = move || format!("Hola, {}!", nombre);
println!("{}", aplicar_una_vez(saludo));
// Fn: captura por referencia, reutilizable
let factor = 3u32;
let multiplicar = |x| x * factor;
println!("{:?}", aplicar_veces(multiplicar, 5)); // [0,3,6,9,12]
// FnMut: captura por referencia mutable, acumula estado
let mut acum = 0u32;
let con_acumulador = |v: u32| { acum += v; acum };
println!("{:?}", acumular(con_acumulador, &[1, 2, 3, 4])); // [1,3,6,10]
}
move closures para threads
Cuando pasas una closure a thread::spawn, el hilo nuevo podría sobrevivir al hilo actual. El compilador exige que la closure sea 'static, lo que significa que no puede tener referencias a datos del stack del hilo padre. La solución es move, que transfiere la propiedad de las variables capturadas a la closure.
use std::thread;
fn main() {
let datos = vec![1, 2, 3, 4, 5];
// ERROR sin move: `datos` podría quedar libre antes de que el hilo termine
// let handle = thread::spawn(|| println!("{:?}", datos));
let handle = thread::spawn(move || {
let suma: i32 = datos.iter().sum();
println!("Suma en hilo: {suma}");
});
// `datos` ya no está disponible aquí porque fue movido al hilo
handle.join().unwrap();
// Para usar el valor tanto en el hilo principal como en el secundario:
let compartido = std::sync::Arc::new(vec![10, 20, 30]);
let copia = std::sync::Arc::clone(&compartido);
let h2 = thread::spawn(move || copia.iter().sum::<i32>());
println!("Suma en hijo: {}", h2.join().unwrap());
println!("Original intacto: {:?}", compartido);
}
Retornar closures: impl Fn vs Box<dyn Fn>
Las closures tienen tipos únicos e innombrables generados por el compilador. Para devolverlas desde una función tienes dos opciones: impl Fn (despacho estático, sin heap) cuando el tipo de retorno es siempre el mismo, o Box<dyn Fn> (despacho dinámico) cuando puede variar.
// impl Fn: el compilador conoce el tipo exacto en compilación
fn multiplicador(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
// Box<dyn Fn>: necesario cuando el tipo puede variar en runtime
fn operacion(tipo: &str) -> Box<dyn Fn(i32) -> i32> {
match tipo {
"doble" => Box::new(|x| x * 2),
"triple" => Box::new(|x| x * 3),
"cuadrado" => Box::new(|x| x * x),
_ => Box::new(|x| x),
}
}
fn main() {
let por_cinco = multiplicador(5);
println!("{}", por_cinco(7)); // 35
println!("{}", por_cinco(3)); // 15
let op = operacion("cuadrado");
println!("{}", op(6)); // 36
// Composición de closures
let doble = multiplicador(2);
let triple = multiplicador(3);
let compuesto = move |x| triple(doble(x));
println!("{}", compuesto(4)); // 24
}
Punteros de función: el tipo fn
Los punteros de función (fn en minúscula, sin mayúscula) son tipos que apuntan a funciones nombradas. A diferencia de las closures, no capturan entorno. Implementan los tres traits Fn, FnMut y FnOnce, por lo que pueden usarse donde se espera una closure.
fn sumar_uno(x: i32) -> i32 { x + 1 }
fn restar_uno(x: i32) -> i32 { x - 1 }
fn aplicar(f: fn(i32) -> i32, valor: i32) -> i32 {
f(valor)
}
fn main() {
// Puntero de función pasado directamente
println!("{}", aplicar(sumar_uno, 10)); // 11
println!("{}", aplicar(restar_uno, 10)); // 9
// Array de punteros de función
let ops: [fn(i32) -> i32; 3] = [sumar_uno, restar_uno, |x| x * 2];
let resultados: Vec<i32> = ops.iter().map(|f| f(5)).collect();
println!("{:?}", resultados); // [6, 4, 10]
// Diferencia con closures que capturan
let offset = 100;
// fn no puede capturar `offset`
// fn sumador(x: i32) -> i32 { x + offset } // ERROR
let sumador: fn(i32) -> i32 = sumar_uno; // OK: función nombrada
// Con closure sí funciona
let sumador_closure = |x: i32| x + offset;
println!("{}", sumador_closure(5)); // 105
println!("{}", sumador(5)); // 6
}
La guía práctica: usa impl Fn en los argumentos cuando quieras aceptar tanto funciones nombradas como closures; usa fn como tipo solo cuando necesites almacenar punteros de función en estructuras de datos que no puedan tener closures con entorno capturado.
