Rust
Tutorial Rust: Funciones anónimas closures
Aprende a usar funciones anónimas (closures) en Rust para escribir código conciso y flexible con captura de entorno y uso en iteradores.
Aprende Rust y certifícateFunciones anónimas
En Rust, las funciones anónimas (también conocidas como closures) son una característica fundamental que permite definir funciones sin necesidad de nombrarlas explícitamente. A diferencia de las funciones regulares que declaramos con la palabra clave fn
, las closures ofrecen una sintaxis concisa y la capacidad de ser creadas en el lugar donde se utilizan.
La sintaxis básica de una closure en Rust sigue este patrón:
let closure = |parámetros| expresión;
Donde los parámetros se colocan entre barras verticales |
(similar a cómo otros lenguajes usan paréntesis), seguidos de una expresión que constituye el cuerpo de la función. Veamos un ejemplo simple:
fn main() {
// Una función anónima que suma dos números
let suma = |a, b| a + b;
// Llamamos a la closure como si fuera una función normal
let resultado = suma(5, 3);
println!("El resultado es: {}", resultado); // Imprime: El resultado es: 8
}
Las closures pueden tener tipos inferidos o tipos explícitos. En el ejemplo anterior, Rust infiere automáticamente que a
y b
son del mismo tipo numérico. Si queremos ser explícitos con los tipos, podemos hacerlo así:
let suma: |i32, i32| -> i32 = |a, b| a + b;
Cuando el cuerpo de la closure contiene múltiples expresiones, podemos usar llaves {}
para crear un bloque, similar a las funciones regulares:
let operacion_compleja = |x, y| {
let resultado_intermedio = x * 2;
resultado_intermedio + y
};
println!("Resultado: {}", operacion_compleja(5, 3)); // Imprime: Resultado: 13
Las closures son particularmente útiles cuando necesitamos funciones pequeñas y específicas que solo se utilizan en un contexto limitado. Por ejemplo, son ideales para operaciones de transformación de datos:
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
// Usamos una closure para duplicar cada número
let duplicados: Vec<i32> = numeros.iter()
.map(|n| n * 2)
.collect();
println!("Números duplicados: {:?}", duplicados); // Imprime: Números duplicados: [2, 4, 6, 8, 10]
}
Una ventaja importante de las closures es que pueden ser más concisas que las funciones tradicionales. Comparemos:
// Función tradicional
fn duplicar(x: i32) -> i32 {
x * 2
}
// Equivalente como closure
let duplicar = |x: i32| x * 2;
Las closures también pueden omitir el tipo de retorno, ya que Rust lo infiere automáticamente del valor de la última expresión, igual que en las funciones regulares.
Otra característica interesante es que podemos crear y usar closures directamente en el lugar sin asignarlas a variables:
fn main() {
let resultado = (|x, y| x + y)(10, 5);
println!("Resultado: {}", resultado); // Imprime: Resultado: 15
}
Las closures son especialmente útiles cuando trabajamos con iteradores y sus métodos como map()
, filter()
, fold()
, etc.:
fn main() {
let numeros = vec![1, 2, 3, 4, 5, 6];
// Filtrar números pares
let pares: Vec<&i32> = numeros.iter()
.filter(|n| *n % 2 == 0)
.collect();
println!("Números pares: {:?}", pares); // Imprime: Números pares: [2, 4, 6]
}
En este ejemplo, |n| *n % 2 == 0
es una closure que toma una referencia a un número y devuelve true
si es par. El operador de desreferencia *
es necesario porque filter()
pasa referencias a los elementos.
Las closures también pueden ser almacenadas en estructuras de datos o pasadas como argumentos a otras funciones, lo que las hace extremadamente versátiles:
fn aplicar_operacion<F>(x: i32, y: i32, operacion: F) -> i32
where F: Fn(i32, i32) -> i32
{
operacion(x, y)
}
fn main() {
let suma = |a, b| a + b;
let multiplicacion = |a, b| a * b;
println!("Suma: {}", aplicar_operacion(5, 3, suma)); // Imprime: Suma: 8
println!("Multiplicación: {}", aplicar_operacion(5, 3, multiplicacion)); // Imprime: Multiplicación: 15
}
En este ejemplo, la función aplicar_operacion
acepta cualquier closure que tome dos i32
y devuelva un i32
. La cláusula where F: Fn(i32, i32) -> i32
especifica que el parámetro genérico F
debe ser un tipo que implemente el trait Fn(i32, i32) -> i32
.
Las funciones anónimas en Rust son una herramienta poderosa que permite escribir código más conciso y expresivo, especialmente cuando se trabaja con operaciones de transformación de datos o cuando se necesitan callbacks para eventos específicos.
Captura del entorno
Una de las características más potentes de las closures en Rust es su capacidad para capturar variables de su entorno circundante. A diferencia de las funciones regulares, las closures pueden acceder y utilizar variables definidas fuera de su propio ámbito.
Cuando una closure captura una variable de su entorno, Rust determina automáticamente cómo debe capturarla basándose en lo que la closure hace con esa variable. Existen tres formas principales de captura:
- Por referencia inmutable (
&T
) - Por referencia mutable (
&mut T
) - Por valor (
T
)
Por defecto, Rust utiliza la forma menos restrictiva posible. Veamos un ejemplo básico de captura:
fn main() {
let x = 10;
// Esta closure captura 'x' por referencia inmutable
let imprimir = || println!("x: {}", x);
imprimir(); // Imprime: x: 10
}
En este ejemplo, la closure imprimir
captura la variable x
por referencia inmutable porque solo necesita leerla, no modificarla.
Cuando necesitamos modificar una variable capturada, Rust utilizará automáticamente una captura por referencia mutable:
fn main() {
let mut contador = 0;
// Esta closure captura 'contador' por referencia mutable
let mut incrementar = || {
contador += 1;
println!("Contador: {}", contador);
};
incrementar(); // Imprime: Contador: 1
incrementar(); // Imprime: Contador: 2
}
Es importante notar que cuando una closure captura una variable por referencia mutable, esa variable no puede ser utilizada en ningún otro lugar mientras la closure exista:
fn main() {
let mut valor = 5;
let mut modificar = || valor += 1;
// Esto funciona porque estamos usando 'valor' antes de crear otra referencia
println!("Valor inicial: {}", valor); // Imprime: Valor inicial: 5
modificar();
// Esto también funciona porque usamos 'valor' después de que la referencia mutable
// en la closure ya no está activa
println!("Valor modificado: {}", valor); // Imprime: Valor modificado: 6
// Creamos otra referencia mutable a través de la closure
let otra_ref = &mut modificar;
// Esto NO compilaría si lo descomentamos, porque 'valor' ya está siendo
// referenciado mutablemente por 'otra_ref'
// println!("Valor actual: {}", valor);
otra_ref();
// Ahora sí podemos usar 'valor' de nuevo
println!("Valor final: {}", valor); // Imprime: Valor final: 7
}
Captura por valor con move
En ocasiones, necesitamos que una closure tome posesión de las variables que captura, en lugar de simplemente referenciarlas. Para esto, Rust proporciona la palabra clave move
:
fn main() {
let texto = String::from("Hola");
// Sin 'move', esta closure capturaría 'texto' por referencia
let sin_move = || println!("Sin move: {}", texto);
// Con 'move', la closure toma posesión de 'texto'
let con_move = move || println!("Con move: {}", texto);
// Podemos usar 'sin_move' y luego usar 'texto' porque solo tomó una referencia
sin_move();
println!("Texto original: {}", texto); // Esto funciona
// Después de llamar a 'con_move', ya no podemos usar 'texto'
// porque la closure tomó posesión de ella
con_move();
// Esto NO compilaría si lo descomentamos:
// println!("Intento de acceso: {}", texto);
}
La palabra clave move
es especialmente útil en programación concurrente o cuando necesitamos que una closure sobreviva más allá del ámbito donde fue creada:
use std::thread;
fn main() {
let datos = vec![1, 2, 3];
// Creamos un nuevo hilo que usa 'datos'
// 'move' es necesario aquí porque el hilo podría vivir más que 'datos'
let hilo = thread::spawn(move || {
println!("Procesando datos: {:?}", datos);
});
// Esperamos a que el hilo termine
hilo.join().unwrap();
// No podemos usar 'datos' aquí porque 'move' transfirió su propiedad al hilo
}
Comportamiento de captura según el uso
Rust determina automáticamente el tipo de captura basándose en cómo la closure utiliza las variables:
fn main() {
let mut valor = 5;
let texto = String::from("Hola");
// Captura por referencia inmutable (solo lee 'valor' y 'texto')
let lector = || {
println!("Valor: {}, Texto: {}", valor, texto);
};
// Captura 'valor' por referencia mutable y 'texto' por referencia inmutable
let modificador = || {
valor += 1;
println!("Nuevo valor: {}, Texto: {}", valor, texto);
};
lector(); // Podemos usar 'lector' primero
// Ahora usamos 'modificador', que toma una referencia mutable a 'valor'
modificador();
// Podemos usar 'lector' de nuevo después
lector();
// También podemos seguir usando las variables originales
println!("Valor final: {}, Texto final: {}", valor, texto);
}
Traits de closures y captura
Detrás de escenas, Rust implementa las closures mediante traits. Existen tres traits principales que determinan cómo una closure puede capturar y usar su entorno:
FnOnce
: Closures que pueden ser llamadas una sola vez. Toman posesión de las variables capturadas.FnMut
: Closures que pueden modificar su entorno capturado. Pueden ser llamadas múltiples veces.Fn
: Closures que solo leen de su entorno. También pueden ser llamadas múltiples veces.
Estos traits forman una jerarquía: Fn
es un subtrait de FnMut
, que a su vez es un subtrait de FnOnce
. Esto significa que una closure que implementa Fn
también implementa FnMut
y FnOnce
.
fn ejecutar_una_vez<F>(closure: F)
where F: FnOnce()
{
closure();
// No podemos llamar a 'closure()' de nuevo aquí
}
fn ejecutar_varias_veces<F>(mut closure: F)
where F: FnMut()
{
closure();
closure(); // Podemos llamarla múltiples veces
}
fn main() {
let texto = String::from("Hola");
// Esta closure implementa FnOnce porque toma posesión de 'texto'
let consume = move || {
let _s = texto;
println!("Consumido");
};
ejecutar_una_vez(consume);
// Esta closure implementa FnMut (y por tanto FnOnce)
let mut contador = 0;
let incrementar = || {
contador += 1;
println!("Contador: {}", contador);
};
ejecutar_varias_veces(incrementar);
}
La captura del entorno es lo que hace que las closures sean tan flexibles y útiles en Rust, permitiéndote crear funciones que mantienen estado o que interactúan con su contexto circundante de manera segura y controlada.
Closures como argumentos
Uno de los usos más comunes de las closures en Rust es pasarlas como argumentos a otras funciones. Esta capacidad permite crear código altamente flexible y reutilizable, donde el comportamiento específico puede ser inyectado en tiempo de ejecución.
Para aceptar una closure como argumento, una función debe declarar un parámetro genérico con restricciones de trait apropiadas. Veamos un ejemplo básico:
fn ejecutar_operacion<F>(a: i32, b: i32, operacion: F) -> i32
where F: Fn(i32, i32) -> i32
{
operacion(a, b)
}
fn main() {
let suma = |x, y| x + y;
let producto = |x, y| x * y;
println!("Suma: {}", ejecutar_operacion(5, 3, suma)); // Imprime: Suma: 8
println!("Producto: {}", ejecutar_operacion(5, 3, producto)); // Imprime: Producto: 15
}
En este ejemplo, la función ejecutar_operacion
acepta cualquier closure que tome dos parámetros i32
y devuelva un i32
. La cláusula where F: Fn(i32, i32) -> i32
especifica esta restricción.
Closures con iteradores
Las closures brillan especialmente cuando se combinan con los métodos de iteradores en Rust. Estos métodos aceptan closures para personalizar su comportamiento:
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
// Usando map() con una closure para transformar cada elemento
let cuadrados: Vec<i32> = numeros.iter()
.map(|n| n * n)
.collect();
println!("Cuadrados: {:?}", cuadrados); // Imprime: Cuadrados: [1, 4, 9, 16, 25]
// Usando filter() con una closure para seleccionar elementos
let mayores_que_tres: Vec<&i32> = numeros.iter()
.filter(|&&n| n > 3)
.collect();
println!("Mayores que tres: {:?}", mayores_que_tres); // Imprime: Mayores que tres: [4, 5]
}
Podemos encadenar múltiples operaciones con iteradores, cada una con su propia closure:
fn main() {
let datos = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let resultado: Vec<i32> = datos.iter()
.filter(|&&x| x % 2 == 0) // Seleccionar solo números pares
.map(|&x| x * 3) // Multiplicar cada número por 3
.filter(|&x| x > 10) // Seleccionar solo resultados mayores que 10
.collect();
println!("Resultado: {:?}", resultado); // Imprime: Resultado: [12, 18, 24, 30]
}
Especificando el trait correcto
Dependiendo de cómo la closure interactúa con sus argumentos y el entorno, necesitaremos especificar diferentes traits:
// Para closures que solo leen sus argumentos
fn procesar_datos<F>(datos: &[i32], procesador: F) -> Vec<i32>
where F: Fn(&i32) -> i32
{
datos.iter().map(procesador).collect()
}
// Para closures que modifican su estado interno
fn acumular<F>(datos: &[i32], mut acumulador: F) -> i32
where F: FnMut(i32, i32) -> i32
{
let mut resultado = 0;
for &valor in datos {
resultado = acumulador(resultado, valor);
}
resultado
}
// Para closures que consumen sus argumentos
fn consumir_ultimo<F>(datos: Vec<String>, consumidor: F)
where F: FnOnce(String)
{
if let Some(ultimo) = datos.last().cloned() {
consumidor(ultimo);
}
}
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
// Usando Fn
let duplicados = procesar_datos(&numeros, |&n| n * 2);
println!("Duplicados: {:?}", duplicados); // Imprime: Duplicados: [2, 4, 6, 8, 10]
// Usando FnMut
let suma = acumular(&numeros, |acc, x| acc + x);
println!("Suma: {}", suma); // Imprime: Suma: 15
// Usando FnOnce
let palabras = vec![
String::from("hola"),
String::from("mundo")
];
consumir_ultimo(palabras, |s| println!("Última palabra: {}", s)); // Imprime: Última palabra: mundo
}
Closures como callbacks
Las closures son ideales para implementar callbacks, permitiendo que una función notifique a otra cuando ocurre un evento específico:
struct Notificador<F>
where F: Fn(&str)
{
enviar_notificacion: F,
}
impl<F> Notificador<F>
where F: Fn(&str)
{
fn new(callback: F) -> Self {
Notificador {
enviar_notificacion: callback,
}
}
fn notificar(&self, mensaje: &str) {
(self.enviar_notificacion)(mensaje);
}
}
fn main() {
// Creamos un notificador con una closure como callback
let notificador = Notificador::new(|msg| {
println!("NOTIFICACIÓN: {}", msg);
});
notificador.notificar("Actualización disponible"); // Imprime: NOTIFICACIÓN: Actualización disponible
notificador.notificar("Proceso completado"); // Imprime: NOTIFICACIÓN: Proceso completado
}
Closures en métodos de colecciones
Además de los iteradores, muchos métodos de colecciones en Rust aceptan closures para personalizar su comportamiento:
fn main() {
let mut numeros = vec![4, 2, 5, 1, 3];
// Ordenar usando una closure como comparador
numeros.sort_by(|a, b| a.cmp(b));
println!("Ordenados ascendente: {:?}", numeros); // Imprime: Ordenados ascendente: [1, 2, 3, 4, 5]
// Ordenar en orden descendente
numeros.sort_by(|a, b| b.cmp(a));
println!("Ordenados descendente: {:?}", numeros); // Imprime: Ordenados descendente: [5, 4, 3, 2, 1]
// Buscar el primer elemento que cumple una condición
let numeros_variados = vec![10, 25, 3, 42, 8];
let primer_mayor_que_20 = numeros_variados.iter().find(|&&x| x > 20);
if let Some(&valor) = primer_mayor_que_20 {
println!("Primer valor mayor que 20: {}", valor); // Imprime: Primer valor mayor que 20: 25
}
}
Combinando closures con tipos de retorno
Podemos crear funciones que devuelvan closures para construir comportamientos personalizados:
fn crear_verificador(umbral: i32) -> impl Fn(i32) -> bool {
move |valor| valor > umbral
}
fn main() {
let numeros = vec![5, 10, 15, 20, 25, 30];
// Creamos diferentes verificadores con diferentes umbrales
let mayor_que_10 = crear_verificador(10);
let mayor_que_20 = crear_verificador(20);
// Filtramos usando los verificadores
let filtrados_10: Vec<i32> = numeros.iter()
.filter(|&&n| mayor_que_10(n))
.cloned()
.collect();
let filtrados_20: Vec<i32> = numeros.iter()
.filter(|&&n| mayor_que_20(n))
.cloned()
.collect();
println!("Mayores que 10: {:?}", filtrados_10); // Imprime: Mayores que 10: [15, 20, 25, 30]
println!("Mayores que 20: {:?}", filtrados_20); // Imprime: Mayores que 20: [25, 30]
}
En este ejemplo, crear_verificador
devuelve una closure que captura el valor umbral
mediante move
. La palabra clave impl Fn(i32) -> bool
indica que la función devuelve algún tipo que implementa el trait Fn(i32) -> bool
.
Closures en operaciones de reducción
Las closures son fundamentales en operaciones de reducción como fold
y reduce
:
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
// Sumar todos los elementos usando fold
let suma = numeros.iter().fold(0, |acumulador, &elemento| acumulador + elemento);
println!("Suma: {}", suma); // Imprime: Suma: 15
// Encontrar el máximo usando fold
let maximo = numeros.iter().fold(i32::MIN, |max, &elemento| max.max(elemento));
println!("Máximo: {}", maximo); // Imprime: Máximo: 5
// Crear una cadena a partir de los números
let cadena = numeros.iter().fold(String::new(), |mut acc, &num| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(&num.to_string());
acc
});
println!("Cadena: {}", cadena); // Imprime: Cadena: 1, 2, 3, 4, 5
}
Las closures como argumentos son una herramienta esencial en el arsenal de Rust, permitiendo crear código modular, flexible y expresivo que puede adaptarse a diferentes situaciones sin sacrificar la seguridad ni el rendimiento.
Otras lecciones de Rust
Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Rust
Introducción Y Entorno
Primer Programa
Introducción Y Entorno
Instalación Del Entorno
Introducción Y Entorno
Funciones
Sintaxis
Operadores
Sintaxis
Estructuras De Control Condicional
Sintaxis
Arrays Y Strings
Sintaxis
Manejo De Errores Panic
Sintaxis
Variables Y Tipos Básicos
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Colecciones Estándar
Estructuras De Datos
Option Y Result
Estructuras De Datos
Pattern Matching
Estructuras De Datos
Estructuras (Structs)
Estructuras De Datos
Enumeraciones Enums
Estructuras De Datos
El Concepto De Ownership
Ownership
Lifetimes Básicos
Ownership
Slices Y Referencias Parciales
Ownership
References Y Borrowing
Ownership
Funciones Anónimas Closures
Abstracción
Traits De La Biblioteca Estándar
Abstracción
Traits
Abstracción
Generics
Abstracción
Channels Y Paso De Mensajes
Concurrencia
Memoria Compartida Segura
Concurrencia
Threads Y Sincronización Básica
Concurrencia
Introducción A Tokio
Asincronía
Fundamentos Asíncronos Y Futures
Asincronía
Async/await
Asincronía
Ejercicios de programación de Rust
Evalúa tus conocimientos de esta lección Funciones anónimas closures con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la sintaxis y definición básica de closures en Rust.
- Aprender cómo las closures capturan variables del entorno y las diferentes formas de captura.
- Identificar los traits asociados a las closures: FnOnce, FnMut y Fn.
- Saber cómo pasar closures como argumentos a funciones y su uso con iteradores.
- Aplicar closures en contextos prácticos como callbacks, ordenación y operaciones de reducción.