Closures en Rust: Fn, FnMut, FnOnce, capturas y la diferencia con funciones

Los closures en Rust son funciones anónimas que pueden capturar variables del entorno donde se definen. A diferencia de otros lenguajes, Rust es explícito sobre cómo se captura ese entorno: por referencia inmutable, por referencia mutable o por valor. De esa captura depende cuál de los tres traits —Fn, FnMut o FnOnce— implementa el closure, y eso determina dónde y cuántas veces puedes llamarlo.

Sintaxis y tipos inferidos

fn main() {
    // Función normal
    fn cuadrado(x: i32) -> i32 { x * x }

    // Closure con tipos explícitos
    let cuadrado_c = |x: i32| -> i32 { x * x };

    // El compilador infiere los tipos
    let cuadrado_i = |x| x * x;

    println!("{}", cuadrado(5));   // 25
    println!("{}", cuadrado_c(5)); // 25
    println!("{}", cuadrado_i(5)); // 25

    // Closure de una expresión: las llaves son opcionales
    let doble = |x| x * 2;
    println!("{}", doble(7)); // 14
}

Captura del entorno

fn main() {
    let factor = 3;

    // Captura por referencia inmutable (&factor)
    let multiplica = |x| x * factor;
    println!("{}", multiplica(5)); // 15
    println!("factor sigue: {factor}"); // 3

    let mut contador = 0;

    // Captura por referencia mutable (&mut contador)
    let mut incrementar = || {
        contador += 1;
        println!("contador: {contador}");
    };
    incrementar(); // 1
    incrementar(); // 2
    // println!("{contador}"); // ERROR: mientras exista `incrementar`, contador está prestado
    drop(incrementar);
    println!("final: {contador}"); // 2
}

move: capturar por valor

use std::thread;

fn main() {
    let mensaje = String::from("hola desde el hilo");

    // move fuerza que el closure tome propiedad de mensaje
    let h = thread::spawn(move || {
        println!("{mensaje}");
    });

    // println!("{mensaje}"); // ERROR: mensaje fue movido al closure
    h.join().unwrap();
}

Fn, FnMut y FnOnce

Rust asigna automáticamente el trait más restrictivo que necesita el closure:

  • FnOnce: puede llamarse una sola vez. Consume variables capturadas.
  • FnMut: puede llamarse varias veces y muta el entorno capturado.
  • Fn: puede llamarse varias veces sin mutar nada.
fn llamar_una_vez<F: FnOnce() -> String>(f: F) {
    println!("{}", f());
    // f(); // ERROR: f ya fue consumido
}

fn llamar_varias<F: Fn() -> i32>(f: F, veces: u32) {
    for _ in 0..veces {
        println!("{}", f());
    }
}

fn acumular<F: FnMut(i32) -> i32>(mut f: F, valores: &[i32]) -> Vec<i32> {
    valores.iter().map(|&v| f(v)).collect()
}

fn main() {
    let s = String::from("consumido");
    llamar_una_vez(move || s); // FnOnce: consume s

    llamar_varias(|| 42, 3); // Fn: solo lee

    let mut suma = 0;
    let resultados = acumular(|x| { suma += x; suma }, &[1, 2, 3, 4]);
    println!("{resultados:?}"); // [1, 3, 6, 10]
}

impl Fn vs Box<dyn Fn> como parámetros

// impl Fn: genérico, resuelto en compilación (zero-cost)
fn aplicar_impl(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

// Box<dyn Fn>: despacho dinámico, sirve para guardar closures en structs
fn crear_multiplicador(n: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x * n)
}

struct Pipeline {
    pasos: Vec<Box<dyn Fn(i32) -> i32>>,
}

impl Pipeline {
    fn new() -> Self { Self { pasos: vec![] } }
    fn add(mut self, f: impl Fn(i32) -> i32 + 'static) -> Self {
        self.pasos.push(Box::new(f));
        self
    }
    fn run(&self, entrada: i32) -> i32 {
        self.pasos.iter().fold(entrada, |acc, f| f(acc))
    }
}

fn main() {
    let triple = crear_multiplicador(3);
    println!("{}", triple(5)); // 15

    let resultado = Pipeline::new()
        .add(|x| x + 1)
        .add(|x| x * 2)
        .add(|x| x - 3)
        .run(10);
    println!("{resultado}"); // (10+1)*2-3 = 19
}

Closures que acumulan estado

fn contador_desde(inicio: i32) -> impl FnMut() -> i32 {
    let mut n = inicio;
    move || {
        let actual = n;
        n += 1;
        actual
    }
}

fn main() {
    let mut c = contador_desde(5);
    println!("{}", c()); // 5
    println!("{}", c()); // 6
    println!("{}", c()); // 7
}

Resumen

  • Los closures capturan el entorno por referencia, referencia mutable o valor (move).
  • Fn: solo lee el entorno; puede llamarse sin límite.
  • FnMut: muta el entorno; puede llamarse sin límite.
  • FnOnce: consume el entorno; solo puede llamarse una vez.
  • Usa impl Fn para parámetros de función genéricos (más rápido).
  • Usa Box<dyn Fn> cuando necesitas guardar closures en estructuras de datos.
  • move es obligatorio cuando el closure cruza límites de hilo.

COMPARTE ESTE ARTÍCULO

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