¿Quién demonios son Fn, FnMut y FnOnce? Los closures de Rust bajo el microscopio

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Closures en Rust: ¿magia o matemática estricta?

\n\n

Si alguna vez has escrito algo como |x| x + 1 en Rust, has usado closures. Pero en cuanto intentas pasar ese closure a una función o guardarlo en una estructura, el compilador de repente empieza a exigir: «¿Qué trait? ¿Fn, FnMut o FnOnce?».

\n\n

Y aquí muchos se quedan perplejos. Especialmente si vienes de JavaScript, Python o Ruby, donde los closures son «simplemente funciones que capturan variables». En Rust es más complicado (e interesante).

\n\n

Hoy entenderemos qué sucede realmente bajo el capó cuando Rust decide qué trait asignar a tu closure. No es magia: es un sistema de tipos estricto que te protege de condiciones de carrera y punteros colgantes. ¡Allá vamos!

\n\n

Resumen rápido: los tres traits de closures

\n\n

En Rust hay tres traits que los closures implementan:

\n\n
    \n
  • FnOnce — puede ser invocado una sola vez. Toma posesión de las variables capturadas.
  • \n
  • FnMut — puede ser invocado múltiples veces, pero puede modificar las variables capturadas (toma &mut).
  • \n
  • Fn — puede ser invocado múltiples veces y no modifica las variables capturadas (toma &).
  • \n
\n\n

Cada closure implementa automáticamente todos los traits aplicables. Es decir, si un closure implementa Fn, también implementa automáticamente FnMut y FnOnce. Pero al revés no funciona.

\n\n

¿Por qué? Porque Fn es el más restrictivo, y FnOnce el más débil. La jerarquía de herencia es la siguiente:

\n\n
FnOnce  ←  FnMut  ←  Fn
\n\n

Es decir, FnMut: FnOnce, y Fn: FnMut. Recuerda esta escalera: es clave.

\n\n

¿Cómo decide Rust qué trait asignar a un closure?

\n\n

El compilador observa cómo exactamente el closure usa las variables capturadas. Hay tres modos de captura:

\n\n
    \n
  • Por referencia (&T) — el closure solo lee la variable.
  • \n
  • Por referencia mutable (&mut T) — el closure modifica la variable.
  • \n
  • Por valor (T) — el closure toma posesión de la variable.
  • \n
\n\n

Así es como esto afecta a los traits:

\n\n\n \n \n \n \n
Modo de capturaTraits implementadosEjemplo
Solo &TFn + FnMut + FnOnce|| println!("{}", x)
&mut T (pero no T)FnMut + FnOnce|| x += 1
T (posesión)Solo FnOnce|| drop(x) o move || x
\n\n

Analicemos cada caso con código real.

\n\n

Ejemplo 1: Solo lectura → Fn

\n\n
let x = 42;\nlet read_closure = || println!("x = {}", x);\n\n// Se puede invocar tantas veces como se quiera\nread_closure();\nread_closure();\n\n// E incluso pasarlo a una función que requiera Fn\nfn call_fn<F: Fn()>(f: F) {\n    f();\n}\ncall_fn(read_closure);
\n\n

Aquí el closure simplemente lee x. No necesita modificarlo ni tomar posesión. Rust entiende: «Ok, esto es seguro — le damos Fn».

\n\n

Ejemplo 2: Modificación → FnMut

\n\n
let mut x = 42;\nlet mut mut_closure = || {\n    x += 1;\n    println!("x = {}", x);\n};\n\nmut_closure(); // x = 43\nmut_closure(); // x = 44\n\n// Lo pasamos a una función que requiera FnMut\nfn call_fn_mut<F: FnMut()>(mut f: F) {\n    f();\n    f();\n}\ncall_fn_mut(mut_closure);
\n\n

Observa que declaramos mut_closure como let mut. ¿Por qué? Porque invocar FnMut requiere una referencia mutable

Blogs

Book Recommendations