Los closures son funciones anónimas que pueden capturar variables del entorno donde se definen. Son la base de los iteradores, los hilos y muchos patrones idiomáticos de Rust. Entender cómo capturan el entorno por referencia, por referencia mutable o por valor y cuál de los tres traits Fn, FnMut, FnOnce implementan es fundamental para usar Rust de forma efectiva.
Sintaxis básica
// Función normal
fn cuadrado(x: i32) -> i32 { x * x }
// Closure equivalente
let cuadrado = |x: i32| -> i32 { x * x };
// El compilador infiere los tipos
let cuadrado = |x| x * x;
println!("{}", cuadrado(5)); // 25
Captura del entorno
let factor = 3;
let multiplicar = |x| x * factor; // captura factor por referencia
println!("{}", multiplicar(5)); // 15
println!("{}", factor); // factor sigue disponible
El compilador infiere el modo de captura mínimo necesario:
- Por referencia inmutable (
&T): si el closure solo lee la variable. - Por referencia mutable (
&mut T): si el closure modifica la variable. - Por valor (move): si el closure necesita poseer la variable.
Captura mutable
let mut contador = 0;
let mut incrementar = || {
contador += 1;
println!("Contador: {}", contador);
};
incrementar(); // Contador: 1
incrementar(); // Contador: 2
// println!("{}", contador); // ERROR mientras incrementar existe
Captura por valor con move
let mensaje = String::from("hola");
let hilo = std::thread::spawn(move || {
println!("{}", mensaje); // toma posesión de mensaje
});
// println!("{}", mensaje); // ERROR: se movió al closure
hilo.join().unwrap();
move fuerza la captura por valor. Es imprescindible para closures que se pasan a hilos, donde el closure puede sobrevivir al scope donde se definió.
Los tres traits de closure
Todos los closures implementan al menos uno de estos tres traits, de más restrictivo a menos:
FnOnce: el closure puede llamarse al menos una vez. Puede consumir las variables capturadas.FnMut: el closure puede llamarse varias veces y puede modificar las capturas.Fn: el closure puede llamarse varias veces sin modificar las capturas.
fn llamar_una_vez<F: FnOnce()>(f: F) { f(); }
fn llamar_muchas<F: FnMut()>(mut f: F) { f(); f(); f(); }
fn llamar_inmutable<F: Fn()>(f: F) { f(); f(); }
let s = String::from("hola");
llamar_una_vez(|| drop(s)); // FnOnce: consume s
let mut n = 0;
llamar_muchas(|| n += 1); // FnMut: modifica n
let m = 42;
llamar_inmutable(|| println!("{}", m)); // Fn: solo lee
Closures como parámetros
fn aplicar<F: Fn(i32) -> i32>(f: F, valor: i32) -> i32 {
f(valor)
}
fn main() {
let doble = |x| x * 2;
println!("{}", aplicar(doble, 5)); // 10
println!("{}", aplicar(|x| x + 1, 5)); // 6
}
Closures como valor de retorno
fn crear_sumador(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let suma5 = crear_sumador(5);
let suma10 = crear_sumador(10);
println!("{}", suma5(3)); // 8
println!("{}", suma10(3)); // 13
}
Closures con iteradores
let numeros = vec![1, 2, 3, 4, 5];
let pares: Vec<_> = numeros.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.collect();
println!("{:?}", pares); // [4, 16]
Resumen
- Los closures capturan el entorno por referencia, referencia mutable o valor (
move). - El compilador infiere el modo de captura mínimo necesario.
Fn: lectura.FnMut: modificación.FnOnce: consumo.- Usa
movepara closures que pasan a hilos u otros contextos con mayor lifetime. - Los closures son la base de los iteradores:
map,filter,fold
El siguiente artículo profundiza en los iteradores: el trait Iterator, la diferencia entre iter() e into_iter(), y los adaptadores y consumidores más importantes.
