Closures: Funciones Anónimas que Capturan su Entorno

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Closures: Funciones Anónimas que Capturan su Entorno. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, explorarás los closures en Rust, que son funciones anónimas que se pueden guardar en variables o pasar como argumentos, lo que permite la reutilización de código y la personalización de comportamiento al capturar valores de su ámbito de definición.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/mutable_variables -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/integer_types -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/for_loop -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/function_syntax -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/expressions_statements -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/method_syntax -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/traits -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} rust/operator_overloading -.-> lab-100424{{"Closures: Funciones Anónimas que Capturan su Entorno"}} end

Closures: Funciones Anónimas que Capturan su Entorno

Los closures de Rust son funciones anónimas que puedes guardar en una variable o pasar como argumentos a otras funciones. Puedes crear el closure en un lugar y luego llamar al closure en otro lugar para evaluarlo en un contexto diferente. A diferencia de las funciones, los closures pueden capturar valores del ámbito en el que se definen. Demostraremos cómo estas características de los closures permiten la reutilización de código y la personalización de comportamiento.

Capturando el Entorno con Closures

Primero examinaremos cómo podemos usar closures para capturar valores del entorno en el que se definen para su uso posterior. Aquí está el escenario: de vez en cuando, nuestra empresa de camisetas regala una camiseta exclusiva y limitada a alguien en nuestra lista de correo como promoción. Las personas en la lista de correo pueden opcionalmente agregar su color favorito a su perfil. Si la persona elegida para una camiseta gratis tiene su color favorito definido, obtiene la camiseta de ese color. Si la persona no ha especificado un color favorito, obtiene el color que la empresa tiene en mayor cantidad en este momento.

Hay muchas maneras de implementar esto. Para este ejemplo, vamos a usar un enum llamado ShirtColor que tiene las variantes Red y Blue (limitando el número de colores disponibles por simplicidad). Representamos el inventario de la empresa con una struct Inventory que tiene un campo llamado shirts que contiene un Vec<ShirtColor> que representa los colores de camisetas actualmente en stock. El método giveaway definido en Inventory obtiene la preferencia opcional de color de camiseta del ganador de la camiseta gratis y devuelve el color de la camiseta que la persona recibirá. Esta configuración se muestra en la Lista 13-1.

Nombre de archivo: src/main.rs

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(
        &self,
        user_preference: Option<ShirtColor>,
    ) -> ShirtColor {
      1 user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
      2 shirts: vec![
            ShirtColor::Blue,
            ShirtColor::Red,
            ShirtColor::Blue,
        ],
    };

    let user_pref1 = Some(ShirtColor::Red);
  3 let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
  4 let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

Lista 13-1: Situación de regalo de camisetas de la empresa

La store definida en main tiene dos camisetas azules y una roja restantes para distribuir en esta promoción limitada [2]. Llamamos al método giveaway para un usuario con preferencia por una camiseta roja [3] y un usuario sin ninguna preferencia [4].

Una vez más, este código podría implementarse de muchas maneras, y aquí, para centrarse en los closures, hemos adherido a conceptos que ya has aprendido, excepto el cuerpo del método giveaway que utiliza un closure. En el método giveaway, obtenemos la preferencia del usuario como un parámetro de tipo Option<ShirtColor> y llamamos al método unwrap_or_else en user_preference [1]. El método unwrap_or_else en Option<T> está definido por la biblioteca estándar. Toma un argumento: un closure sin ningún argumento que devuelve un valor T (el mismo tipo almacenado en la variante Some de Option<T>, en este caso ShirtColor). Si Option<T> es la variante Some, unwrap_or_else devuelve el valor dentro de Some. Si Option<T> es la variante None, unwrap_or_else llama al closure y devuelve el valor devuelto por el closure.

Especificamos la expresión de closure || self.most_stocked() como argumento para unwrap_or_else. Este es un closure que no toma parámetros por sí mismo (si el closure tuviera parámetros, aparecerían entre los dos tubos verticales). El cuerpo del closure llama a self.most_stocked(). Estamos definiendo el closure aquí, y la implementación de unwrap_or_else evaluará el closure más tarde si es necesario.

Ejecutar este código imprime lo siguiente:

The user with preference Some(Red) gets Red
The user with preference None gets Blue

Un aspecto interesante aquí es que hemos pasado un closure que llama a self.most_stocked() en la instancia actual de Inventory. La biblioteca estándar no necesita saber nada sobre los tipos Inventory o ShirtColor que definimos, ni sobre la lógica que queremos usar en este escenario. El closure captura una referencia inmutable a la instancia self de Inventory y la pasa con el código que especificamos al método unwrap_or_else. Las funciones, por otro lado, no pueden capturar su entorno de esta manera.

Inferencia y Anotación de Tipo de Closure

Hay más diferencias entre funciones y closures. Los closures generalmente no requieren que anotes los tipos de los parámetros o el valor de retorno como lo hacen las funciones fn. Las anotaciones de tipo son necesarias en las funciones porque los tipos son parte de una interfaz explícita expuesta a tus usuarios. Definir esta interfaz rigidamente es importante para garantizar que todos estén de acuerdo sobre qué tipos de valores utiliza y devuelve una función. Los closures, por otro lado, no se utilizan en una interfaz expuesta como esta: se almacenan en variables y se utilizan sin nombrarlas y expuestas a los usuarios de nuestra biblioteca.

Los closures suelen ser cortos y solo son relevantes dentro de un contexto limitado en lugar de en cualquier escenario arbitrario. Dentro de estos contextos limitados, el compilador puede inferir los tipos de los parámetros y el tipo de retorno, de manera similar a cómo es capaz de inferir los tipos de la mayoría de las variables (hay casos raros en los que el compilador también necesita anotaciones de tipo de closure).

Al igual que con las variables, podemos agregar anotaciones de tipo si queremos aumentar la claridad y la explicitud a costa de ser más verboso de lo estrictamente necesario. Anotar los tipos para un closure se vería como la definición mostrada en la Lista 13-2. En este ejemplo, estamos definiendo un closure y almacenándolo en una variable en lugar de definir el closure en el lugar donde lo pasamos como argumento, como hicimos en la Lista 13-1.

Nombre de archivo: src/main.rs

let expensive_closure = |num: u32| -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    num
};

Lista 13-2: Agregando anotaciones opcionales de los tipos de parámetro y valor de retorno en el closure

Con las anotaciones de tipo agregadas, la sintaxis de los closures se parece más a la sintaxis de las funciones. Aquí, definimos una función que suma 1 a su parámetro y un closure que tiene el mismo comportamiento, para comparación. Hemos agregado algunos espacios para alinear las partes relevantes. Esto ilustra cómo la sintaxis de los closures es similar a la sintaxis de las funciones excepto por el uso de tubos y la cantidad de sintaxis que es opcional:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

La primera línea muestra una definición de función y la segunda línea muestra una definición de closure completamente anotada. En la tercera línea, eliminamos las anotaciones de tipo de la definición de closure. En la cuarta línea, eliminamos los corchetes, que son opcionales porque el cuerpo del closure tiene solo una expresión. Estas son todas definiciones válidas que producirán el mismo comportamiento cuando se llamen. Las líneas add_one_v3 y add_one_v4 requieren que se evalúen los closures para poder compilar porque los tipos se inferirán a partir de su uso. Esto es similar a let v = Vec::new(); que necesita anotaciones de tipo o valores de algún tipo para ser insertados en el Vec para que Rust pueda inferir el tipo.

Para las definiciones de closure, el compilador inferirá un tipo concrete para cada uno de sus parámetros y para su valor de retorno. Por ejemplo, la Lista 13-3 muestra la definición de un closure corto que solo devuelve el valor que recibe como parámetro. Este closure no es muy útil excepto con fines de este ejemplo. Nota que no hemos agregado ninguna anotación de tipo a la definición. Debido a que no hay anotaciones de tipo, podemos llamar al closure con cualquier tipo, lo que hemos hecho aquí con String la primera vez. Si luego intentamos llamar a example_closure con un entero, obtendremos un error.

Nombre de archivo: src/main.rs

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);

Lista 13-3: Intentando llamar a un closure cuyos tipos se inferen con dos tipos diferentes

El compilador nos da este error:

error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |                             ^- help: try using a conversion method:
`.to_string()`
  |                             |
  |                             expected struct `String`, found integer

La primera vez que llamamos a example_closure con el valor String, el compilador infiere el tipo de x y el tipo de retorno del closure como String. Esos tipos se bloquean luego en el closure en example_closure, y obtenemos un error de tipo cuando intentamos usar un tipo diferente con el mismo closure a continuación.

Capturando Referencias o Transferiendo la Propiedad

Los closures pueden capturar valores de su entorno de tres maneras, lo cual se mapea directamente a las tres maneras en las que una función puede tomar un parámetro: prestar inmutablemente, prestar mutablemente y tomar la propiedad. El closure decidirá cuál de estas utilizará en función de lo que haga el cuerpo de la función con los valores capturados.

En la Lista 13-4, definimos un closure que captura una referencia inmutable al vector llamado list porque solo necesita una referencia inmutable para imprimir el valor.

Nombre de archivo: src/main.rs

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

  1 let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
  2 only_borrows();
    println!("After calling closure: {:?}", list);
}

Lista 13-4: Definiendo y llamando un closure que captura una referencia inmutable

Este ejemplo también ilustra que una variable puede enlazarse a una definición de closure [1], y luego podemos llamar al closure usando el nombre de la variable y paréntesis como si el nombre de la variable fuera el nombre de una función [2].

Debido a que podemos tener múltiples referencias inmutables a list al mismo tiempo, list sigue siendo accesible desde el código antes de la definición del closure, después de la definición del closure pero antes de que se llame al closure y después de que se llame al closure. Este código se compila, ejecuta y muestra:

Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

A continuación, en la Lista 13-5, cambiamos el cuerpo del closure para que agregue un elemento al vector list. El closure ahora captura una referencia mutable.

Nombre de archivo: src/main.rs

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {:?}", list);
}

Lista 13-5: Definiendo y llamando un closure que captura una referencia mutable

Este código se compila, ejecuta y muestra:

Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

Nota que ya no hay un println! entre la definición y la llamada del closure borrows_mutably: cuando se define borrows_mutably, captura una referencia mutable a list. No usamos el closure nuevamente después de que se llame al closure, por lo que el préstamo mutable finaliza. Entre la definición del closure y la llamada del closure, no se permite un préstamo inmutable para imprimir porque no se permiten otros préstamos cuando hay un préstamo mutable. Intenta agregar un println! allí para ver qué mensaje de error obtienes!

Si quieres forzar al closure a tomar la propiedad de los valores que utiliza en el entorno aunque el cuerpo del closure no necesite estrictamente la propiedad, puedes usar la palabra clave move antes de la lista de parámetros.

Esta técnica es útil principalmente cuando se pasa un closure a un nuevo hilo para mover los datos de modo que queden propiedad del nuevo hilo. Discutiremos los hilos y por qué quisieras usarlos en detalle en el Capítulo 16 cuando hablamos de concurrencia, pero por ahora, exploremos brevemente cómo crear un nuevo hilo usando un closure que necesita la palabra clave move. La Lista 13-6 muestra la Lista 13-4 modificada para imprimir el vector en un nuevo hilo en lugar del hilo principal.

Nombre de archivo: src/main.rs

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

  1 thread::spawn(move || {
      2 println!("From thread: {:?}", list)
    }).join().unwrap();
}

Lista 13-6: Usando move para forzar al closure del hilo a tomar la propiedad de list

Creamos un nuevo hilo, pasando al hilo un closure para que se ejecute como argumento. El cuerpo del closure imprime la lista. En la Lista 13-4, el closure solo capturó list usando una referencia inmutable porque esa es la menor cantidad de acceso a list necesaria para imprimirla. En este ejemplo, aunque el cuerpo del closure todavía solo necesita una referencia inmutable [2], necesitamos especificar que list debe ser movida al closure poniendo la palabra clave move [1] al principio de la definición del closure. El nuevo hilo podría terminar antes de que el resto del hilo principal termine, o el hilo principal podría terminar primero. Si el hilo principal mantiene la propiedad de list pero termina antes del nuevo hilo y libera list, la referencia inmutable en el hilo sería inválida. Por lo tanto, el compilador requiere que list sea movida al closure dado al nuevo hilo para que la referencia sea válida. Intenta quitar la palabra clave move o usar list en el hilo principal después de que se defina el closure para ver qué errores del compilador obtienes!

Sacando los Valores Capturados de los Closures y los Tratamientos Fn

Una vez que un closure ha capturado una referencia o la propiedad de un valor del entorno donde se define el closure (por lo tanto, afectando lo que, si hay algo, se mueve hacia dentro del closure), el código en el cuerpo del closure define lo que pasa con las referencias o valores cuando se evalúa el closure más tarde (por lo tanto, afectando lo que, si hay algo, se mueve hacia fuera del closure).

El cuerpo de un closure puede hacer cualquiera de lo siguiente: mover un valor capturado fuera del closure, mutar el valor capturado, ni mover ni mutar el valor o no capturar nada del entorno para comenzar.

La forma en que un closure captura y maneja valores del entorno afecta a los tratamientos que implementa el closure, y los tratamientos son la forma en que funciones y structs pueden especificar qué tipos de closures pueden usar. Los closures implementarán automáticamente uno, dos o los tres de estos tratamientos Fn, de manera aditiva, dependiendo de cómo el cuerpo del closure maneja los valores:

  • FnOnce se aplica a closures que se pueden llamar una vez. Todos los closures implementan al menos este tratamiento porque todos los closures se pueden llamar. Un closure que mueve valores capturados fuera de su cuerpo solo implementará FnOnce y ninguno de los otros tratamientos Fn porque solo se puede llamar una vez.
  • FnMut se aplica a closures que no mueven valores capturados fuera de su cuerpo, pero que pueden mutar los valores capturados. Estos closures se pueden llamar más de una vez.
  • Fn se aplica a closures que no mueven valores capturados fuera de su cuerpo y que no mutan valores capturados, así como a closures que no capturan nada de su entorno. Estos closures se pueden llamar más de una vez sin mutar su entorno, lo que es importante en casos como llamar a un closure múltiples veces de manera concurrente.

Veamos la definición del método unwrap_or_else en Option<T> que usamos en la Lista 13-1:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

Recuerda que T es el tipo genérico que representa el tipo del valor en la variante Some de un Option. Ese tipo T también es el tipo de retorno de la función unwrap_or_else: el código que llama a unwrap_or_else en un Option<String>, por ejemplo, obtendrá un String.

A continuación, observa que la función unwrap_or_else tiene el parámetro de tipo genérico adicional F. El tipo F es el tipo del parámetro llamado f, que es el closure que proporcionamos cuando llamamos a unwrap_or_else.

La restricción de tratamiento especificada en el tipo genérico F es FnOnce() -> T, lo que significa que F debe poder ser llamado una vez, no tomar argumentos y devolver un T. Usar FnOnce en la restricción de tratamiento expresa la limitación de que unwrap_or_else solo llamará a f una vez, como máximo. En el cuerpo de unwrap_or_else, podemos ver que si el Option es Some, f no se llamará. Si el Option es None, f se llamará una vez. Debido a que todos los closures implementan FnOnce, unwrap_or_else acepta la mayor variedad de closures y es tan flexible como puede ser.

Nota: Las funciones también pueden implementar los tres tratamientos Fn. Si lo que queremos hacer no requiere capturar un valor del entorno, podemos usar el nombre de una función en lugar de un closure donde necesitamos algo que implemente uno de los tratamientos Fn. Por ejemplo, en un valor Option<Vec<T>>, podríamos llamar a unwrap_or_else(Vec::new) para obtener un nuevo vector vacío si el valor es None.

Ahora veamos el método estándar de la biblioteca sort_by_key, definido en slices, para ver cómo difiere de unwrap_or_else y por qué sort_by_key usa FnMut en lugar de FnOnce para la restricción de tratamiento. El closure recibe un argumento en forma de una referencia al elemento actual en el slice que se está considerando y devuelve un valor de tipo K que se puede ordenar. Esta función es útil cuando quieres ordenar un slice por un atributo particular de cada elemento. En la Lista 13-7, tenemos una lista de instancias de Rectangle y usamos sort_by_key para ordenarlas por su atributo width de menor a mayor.

Nombre de archivo: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{:#?}", list);
}

Lista 13-7: Usando sort_by_key para ordenar rectángulos por ancho

Este código imprime:

[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

La razón por la que sort_by_key está definido para tomar un closure FnMut es que lo llama múltiples veces: una vez para cada elemento en el slice. El closure |r| r.width no captura, muta ni mueve nada de su entorno, por lo que cumple con los requisitos de la restricción de tratamiento.

En contraste, la Lista 13-8 muestra un ejemplo de un closure que implementa solo el tratamiento FnOnce, porque mueve un valor fuera del entorno. El compilador no nos permitirá usar este closure con sort_by_key.

Nombre de archivo: src/main.rs

--snip--

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("by key called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{:#?}", list);
}

Lista 13-8: Intentando usar un closure FnOnce con sort_by_key

Este es un ejemplo forzado y complicado (que no funciona) para tratar de contar el número de veces que sort_by_key se llama cuando se ordena list. Este código intenta hacer este recuento empujando value (una String del entorno del closure) al vector sort_operations. El closure captura value y luego mueve value fuera del closure transferiendo la propiedad de value al vector sort_operations. Este closure se puede llamar una vez; intentar llamarlo una segunda vez no funcionaría porque value ya no estaría en el entorno para ser empujado nuevamente a sort_operations! Por lo tanto, este closure solo implementa FnOnce. Cuando intentamos compilar este código, obtenemos este error de que value no se puede mover fuera del closure porque el closure debe implementar FnMut:

error[E0507]: cannot move out of `value`, a captured variable in an `FnMut`
closure
  --> src/main.rs:18:30
   |
15 |       let value = String::from("by key called");
   |           ----- captured outer variable
16 |
17 |       list.sort_by_key(|r| {
   |  ______________________-
18 | |         sort_operations.push(value);
   | |                              ^^^^^ move occurs because `value` has
type `String`, which does not implement the `Copy` trait
19 | |         r.width
20 | |     });
   | |_____- captured by this `FnMut` closure

El error apunta a la línea en el cuerpo del closure que mueve value fuera del entorno. Para solucionar esto, necesitamos cambiar el cuerpo del closure para que no mueva valores fuera del entorno. Mantener un contador en el entorno e incrementar su valor en el cuerpo del closure es una forma más directa de contar el número de veces que se llama a sort_by_key. El closure en la Lista 13-9 funciona con sort_by_key porque solo está capturando una referencia mutable al contador num_sort_operations y por lo tanto se puede llamar más de una vez.

Nombre de archivo: src/main.rs

--snip--

fn main() {
    --snip--

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!(
        "{:#?}, sorted in {num_sort_operations} operations",
        list
    );
}

Lista 13-9: Usar un closure FnMut con sort_by_key está permitido.

Los tratamientos Fn son importantes cuando se definen o se usan funciones o tipos que utilizan closures. En la siguiente sección, discutiremos iteradores. Muchos métodos de iterador toman argumentos de closure, ¡así que mantenga estos detalles de closure en mente a medida que continuamos!

Resumen

¡Felicitaciones! Has completado el laboratorio de Closures: Funciones Anónimas que Capturan su Entorno. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.