Closures en Rust: funciones anónimas que capturan el entorno

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 move para 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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP