Замыкания:анонимные функции, которые захватывают свою окружающую среду

RustRustBeginner
Практиковаться сейчас

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

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Closures: Anonymous Functions That Capture Their Environment. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этой лабораторной работе вы изучите замыкания в Rust, которые являются анонимными функциями, которые можно сохранять в переменных или передавать в качестве аргументов, что позволяет повторно использовать код и настраивать поведение, захватывая значения из их определяющего скоупа.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) 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/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{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/mutable_variables -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/integer_types -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/for_loop -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/function_syntax -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/expressions_statements -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/method_syntax -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/traits -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} rust/operator_overloading -.-> lab-100424{{"Замыкания:анонимные функции, которые захватывают свою окружающую среду"}} end

Замыкания: Анонимные функции, которые захватывают свою среду

Замыкания в Rust - это анонимные функции, которые можно сохранить в переменной или передать в качестве аргументов другим функциям. Вы можете создать замыкание в одном месте, а затем вызвать его в другом месте, чтобы вычислить его в другом контексте. В отличие от функций, замыкания могут захватывать значения из области, в которой они определены. Мы покажем, как эти свойства замыканий позволяют повторно использовать код и настраивать поведение.

Захват окружающей среды с помощью замыканий

Сначала мы рассмотрим, как можно использовать замыкания для захвата значений из окружающей среды, в которой они определены, для последующего использования. Вот сценарий: периодически наша компания по производству футболок раздает эксклюзивную ограниченную серию футболок кому-то из нашей рассылки в качестве продвижения. Люди из рассылки могут по своему усмотрению добавить свой любимый цвет в свой профиль. Если выбранный для бесплатной футболки человек имеет установлен свой любимый цвет, он получает футболку в этом цвете. Если человек не указал любимый цвет, он получает тот цвет, который у компании в данный момент в большем количестве.

Существует множество способов реализовать это. Для примера мы будем использовать перечисление ShirtColor, которое имеет варианты Red и Blue (ограничим количество доступных цветов для простоты). Мы представляем инвентарь компании с помощью структуры Inventory, которая имеет поле shirts, содержащее Vec<ShirtColor>, представляющее цвета футболок, которые в наличии в данный момент. Метод giveaway, определенный для Inventory, получает необязательное предпочтение цвета футболки победителя в розыгрыше и возвращает цвет футболки, который получит человек. Эта схема показана в Листинге 13-1.

Filename: 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
    );
}

Листинг 13-1: Ситуация с розыгрышем футболок компанией

store, определенный в main, имеет по-прежнему два футболки голубого цвета и одну красного цвета для розыгрыша этой ограниченной серии [2]. Мы вызываем метод giveaway для пользователя, который предпочитает красный цвет футболки [3] и для пользователя, у которого нет никаких предпочтений [4].

Здесь снова этот код можно было реализовать многими способами, и здесь, чтобы сосредоточиться на замыканиях, мы придерживались концепций, которые вы уже изучали, за исключением тела метода giveaway, которое использует замыкание. В методе giveaway мы получаем предпочтение пользователя в виде параметра типа Option<ShirtColor> и вызываем метод unwrap_or_else для user_preference [1]. Метод unwrap_or_else для Option<T> определен стандартной библиотекой. Он принимает один аргумент: замыкание без аргументов, которое возвращает значение T (тот же тип, который хранится в варианте Some перечисления Option<T>, в данном случае ShirtColor). Если Option<T> представляет собой вариант Some, метод unwrap_or_else возвращает значение из Some. Если Option<T> представляет собой вариант None, метод unwrap_or_else вызывает замыкание и возвращает значение, возвращаемое замыканием.

Мы указываем выражение замыкания || self.most_stocked() в качестве аргумента для unwrap_or_else. Это замыкание, которое не имеет собственных параметров (если бы у замыкания были параметры, они бы появились между двумя вертикальными трубами). Тело замыкания вызывает self.most_stocked(). Мы определяем здесь замыкание, а реализация unwrap_or_else будет оценивать замыкание позже, если это необходимо.

Запуск этого кода выводит следующее:

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

Одним интересным аспектом здесь является то, что мы передали замыкание, которое вызывает self.most_stocked() для текущего экземпляра Inventory. Стандартная библиотека не должна была знать ничего о типах Inventory или ShirtColor, которые мы определили, или о логике, которую мы хотим использовать в этом сценарии. Замыкание захватывает неизменяемую ссылку на текущий экземпляр Inventory и передает ее вместе с кодом, который мы указываем, методу unwrap_or_else. Функции, с другой стороны, не могут захватывать свою окружающую среду таким образом.

Инференс типа и аннотация для замыканий

Есть и другие различия между функциями и замыканиями. Замыкания обычно не требуют аннотирования типов параметров или возвращаемого значения, как это делают функции fn. Аннотации типов обязательны для функций, потому что типы являются частью явного интерфейса, доступного для ваших пользователей. Тщательное определение этого интерфейса важно для того, чтобы все были согласны по тем типам значений, которые использует и возвращает функция. Замыкания же не используются в таких открытых интерфейсах: они хранятся в переменных и используются без именования и предоставления их пользователям библиотеки.

Замыкания обычно короткие и имеют смысл только в узком контексте, а не в произвольной ситуации. В рамках этих ограниченных контекстов компилятор может вывести типы параметров и тип возвращаемого значения, аналогично тому, как он может вывести типы большинства переменных (есть редкие случаи, когда компилятор также нуждается в аннотациях типов для замыканий).

Как и с переменными, мы можем добавить аннотации типов, если хотим повысить ясность и четкость, жертвуя тем более избыточностью. Аннотация типов для замыкания будет выглядеть как в определении, показанном в Листинге 13-2. В этом примере мы определяем замыкание и сохраняем его в переменной, а не определяем замыкание непосредственно в месте, где передаем его в качестве аргумента, как мы делали в Листинге 13-1.

Filename: src/main.rs

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

Листинг 13-2: Добавление необязательных аннотаций типов параметра и возвращаемого значения в замыкании

После добавления аннотаций типов синтаксис замыканий становится более похож на синтаксис функций. Здесь мы определяем функцию, которая добавляет 1 к своему параметру, и замыкание с тем же поведением для сравнения. Мы добавили несколько пробелов, чтобы выровнять соответствующие части. Это показывает, как синтаксис замыканий похож на синтаксис функций, за исключением использования вертикальных полос и количества необязательного синтаксиса:

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  ;

Первая строка показывает определение функции, а вторая - полностью аннотированное определение замыкания. В третьей строке мы удаляем аннотации типов из определения замыкания. В четвертой строке мы удаляем фигурные скобки, которые являются необязательными, потому что тело замыкания состоит только из одного выражения. Все эти определения валидны и будут иметь одинаковое поведение при вызове. Строки add_one_v3 и add_one_v4 требуют вычисления замыканий для компиляции, потому что типы будут выведены из их использования. Это похоже на то, что let v = Vec::new(); требует либо аннотаций типов, либо значений какого-то типа для вставки в Vec, чтобы Rust мог вывести тип.

Для определений замыканий компилятор будет выводить один конкретный тип для каждого их параметра и для возвращаемого значения. Например, Листинг 13-3 показывает определение короткого замыкания, которое просто возвращает значение, которое получает в качестве параметра. Это замыкание не очень полезно, кроме целей данного примера. Обратите внимание, что мы не добавили никаких аннотаций типов в определение. Поскольку нет аннотаций типов, мы можем вызывать замыкание с любым типом, что мы сделали здесь с String в первый раз. Если мы затем попытаемся вызвать example_closure с целым числом, мы получим ошибку.

Filename: src/main.rs

let example_closure = |x| x;

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

Листинг 13-3: Попытка вызвать замыкание, типы которого выведены, с двумя разными типами

Компилятор выдаёт нам такую ошибку:

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

В первый раз, когда мы вызываем example_closure с значением String, компилятор выводит тип x и тип возвращаемого значения замыкания как String. Эти типы затем фиксируются в замыкании example_closure, и мы получаем ошибку типа, когда затем пытаемся использовать другой тип с тем же замыканием.

Захват ссылок или передача владения

Замыкания могут захватывать значения из своей окружающей среды тремя способами, которые напрямую соответствуют трем видам параметров, которые может принимать функция: неизменяемая ссылка, изменяемая ссылка и передача владения. Замыкание решит, какой из них использовать, исходя из того, что делает тело функции с захваченными значениями.

В Листинге 13-4 мы определяем замыкание, которое захватывает неизменяемую ссылку на вектор с именем list, потому что для вывода значения ему нужна только неизменяемая ссылка.

Filename: 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);
}

Листинг 13-4: Определение и вызов замыкания, которое захватывает неизменяемую ссылку

Этот пример также показывает, что переменная может связываться с определением замыкания [1], и мы позже можем вызвать замыкание, используя имя переменной и круглые скобки, как если бы имя переменной было именем функции [2].

Поскольку мы можем иметь несколько неизменяемых ссылок на list одновременно, list по-прежнему доступен из кода до определения замыкания, после определения замыкания, но до вызова замыкания и после вызова замыкания. Этот код компилируется, запускается и выводит:

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

Далее, в Листинге 13-5, мы меняем тело замыкания так, чтобы оно добавляло элемент в вектор list. Теперь замыкание захватывает изменяемую ссылку.

Filename: 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);
}

Листинг 13-5: Определение и вызов замыкания, которое захватывает изменяемую ссылку

Этот код компилируется, запускается и выводит:

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

Обратите внимание, что между определением и вызовом замыкания borrows_mutably больше нет println!: когда определяется borrows_mutably, оно захватывает изменяемую ссылку на list. Мы не используем замыкание снова после его вызова, поэтому изменяемая ссылка заканчивается. Между определением замыкания и вызовом замыкания не допускается неизменяемая ссылка для вывода, потому что когда есть изменяемая ссылка, не допускаются другие ссылки. Попробуйте добавить println! там, чтобы увидеть, какую ошибку вы получите!

Если вы хотите заставить замыкание принимать владение значениями, которые оно использует в окружающей среде, даже если тело замыкания не строго требует владения, вы можете использовать ключевое слово move перед списком параметров.

Этот метод в основном полезен при передаче замыкания в новый поток, чтобы передать данные, чтобы они были в собственности нового потока. Мы подробно обсудим потоки и почему вы захотите их использовать в главе 16, когда будем говорить о конкурентности, но пока давайте кратко рассмотрим создание нового потока с использованием замыкания, которое требует ключевого слова move. Листинг 13-6 показывает, как Листинг 13-4 изменен для вывода вектора в новом потоке, а не в главном потоке.

Filename: 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();
}

Листинг 13-6: Использование move для заставления замыкания для потока принять владение list

Мы создаем новый поток, передавая потоку замыкание для выполнения в качестве аргумента. Тело замыкания выводит список. В Листинге 13-4 замыкание только захватывало list с использованием неизменяемой ссылки, потому что для вывода этого требуется наименьший доступ к list. В этом примере, хотя тело замыкания по-прежнему требует только неизменяемой ссылки [2], мы должны указать, что list должно быть передано в замыкание, поместив ключевое слово move [1] в начале определения замыкания. Новый поток может завершиться раньше, чем остальная часть главного потока, или главной поток может завершиться раньше. Если главный поток сохраняет владение list, но завершается раньше, чем новый поток, и уничтожает list, неизменяемая ссылка в потоке станет недействительной. Поэтому компилятор требует, чтобы list было передано в замыкание, переданное новому потоку, чтобы ссылка была действительной. Попробуйте удалить ключевое слово move или использовать list в главном потоке после определения замыкания, чтобы увидеть, какие ошибки компиляции вы получите!

Извлечение захваченных значений из замыканий и трейты Fn

После того, как замыкание захватывает ссылку или получает владение значением из окружающей среды, где оно определено (влияние на то, что, если что-то, перемещается в замыкание), код в теле замыкания определяет, что происходит с ссылками или значениями, когда замыкание оценивается позже (влияние на то, что, если что-то, перемещается из замыкания).

Тело замыкания может делать следующее: перемещать захваченное значение из замыкания, изменять захваченное значение, ни перемещать, ни изменять значение или вообще не захватывать ничего из окружающей среды.

Способ, которым замыкание захватывает и обрабатывает значения из окружающей среды, влияет на то, какие трейты реализует замыкание, и трейты - это то, как функции и структуры могут указать, какие виды замыканий они могут использовать. Замыкания автоматически реализуют один, два или все три из этих трейтов Fn в накапливающем порядке, в зависимости от того, как тело замыкания обрабатывает значения:

  • FnOnce применяется к замыканиям, которые можно вызвать один раз. Все замыкания реализуют по крайней мере этот трейт, потому что все замыкания можно вызвать. Замыкание, которое перемещает захваченные значения из своего тела, будет реализовывать только FnOnce и ни один из других трейтов Fn, потому что оно может быть вызвано только один раз.
  • FnMut применяется к замыканиям, которые не перемещают захваченные значения из своего тела, но могут изменять захваченные значения. Эти замыкания можно вызывать более одного раза.
  • Fn применяется к замыканиям, которые не перемещают захваченные значения из своего тела и не изменяют захваченные значения, а также к замыканиям, которые не захватывают ничего из своей окружающей среды. Эти замыкания можно вызывать более одного раза без изменения их окружающей среды, что важно в таких случаях, как вызов замыкания несколько раз одновременно.

Посмотрим на определение метода unwrap_or_else для Option<T>, которое мы использовали в Листинге 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(),
        }
    }
}

Помните, что T - это обобщенный тип, представляющий тип значения в варианте Some перечисления Option. Этот тип T также является типом возвращаемым функцией unwrap_or_else: код, который вызывает unwrap_or_else для Option<String>, например, получит String.

Далее, обратите внимание, что функция unwrap_or_else имеет дополнительный обобщенный параметр типа F. Тип F - это тип параметра с именем f, который является замыканием, которое мы предоставляем при вызове unwrap_or_else.

Ограничение трейта, указанное для обобщенного типа F, - это FnOnce() -> T, что означает, что F должен быть вызываемым один раз, не принимать аргументов и возвращать T. Использование FnOnce в ограничении трейта выражает ограничение, что unwrap_or_else будет вызывать f максимум один раз. В теле unwrap_or_else мы можем увидеть, что если Option равно Some, f не будет вызван. Если Option равно None, f будет вызван один раз. Поскольку все замыкания реализуют FnOnce, unwrap_or_else принимает самую широкую совокупность замыканий и является максимально гибким.

Примечание: Функции также могут реализовать все три трейта Fn. Если то, что мы хотим сделать, не требует захвата значения из окружающей среды, мы можем использовать имя функции вместо замыкания, где нам нужен тип, реализующий один из трейтов Fn. Например, для значения Option<Vec<T>> мы могли бы вызвать unwrap_or_else(Vec::new), чтобы получить новый пустой вектор, если значение равно None.

Теперь посмотрим на метод стандартной библиотеки sort_by_key, определенный для срезов, чтобы понять, как он отличается от unwrap_or_else и почему sort_by_key использует FnMut вместо FnOnce для ограничения трейта. Замыкание получает один аргумент в виде ссылки на текущий элемент в срезе, который рассматривается, и возвращает значение типа K, которое можно упорядочить. Эта функция полезна, когда вы хотите отсортировать срез по определенному атрибуту каждого элемента. В Листинге 13-7 у нас есть список экземпляров Rectangle, и мы используем sort_by_key, чтобы отсортировать их по атрибуту width от меньшего к большему.

Filename: 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);
}

Листинг 13-7: Использование sort_by_key для сортировки прямоугольников по ширине

Этот код выводит:

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

Причина, по которой sort_by_key определен для приема замыкания FnMut, заключается в том, что он вызывает замыкание несколько раз: один раз для каждого элемента в срезе. Замыкание |r| r.width не захватывает, не изменяет и не перемещает ничего из своей окружающей среды, поэтому оно соответствует требованиям ограничения трейта.

В отличие от этого, Листинг 13-8 показывает пример замыкания, которое реализует только трейт FnOnce, потому что оно перемещает значение из окружающей среды. Компилятор не позволит нам использовать это замыкание с sort_by_key.

Filename: 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);
}

Листинг 13-8: Проба использования замыкания FnOnce с sort_by_key

Это искусственный, сложный способ (который не работает), чтобы попробовать подсчитать количество раз, когда sort_by_key вызывается при сортировке list. Этот код пытается сделать это подсчетом, добавляя value - String из окружающей среды замыкания - в вектор sort_operations. Замыкание захватывает value, а затем перемещает value из замыкания, передавая владение value вектору sort_operations. Это замыкание можно вызвать один раз; попытка вызвать его вторично не сработает, потому что value больше не будет в окружающей среде, чтобы снова добавить его в sort_operations! Поэтому это замыкание реализует только FnOnce. Когда мы пытаемся скомпилировать этот код, мы получаем ошибку, что value не может быть перемещено из замыкания, потому что замыкание должно реализовать 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

Ошибка указывает на строку в теле замыкания, которая перемещает value из окружающей среды. Чтобы исправить это, мы должны изменить тело замыкания так, чтобы оно не перемещало значения из окружающей среды. Хранение счетчика в окружающей среде и увеличение его значения в теле замыкания - более простой способ подсчитать количество раз, когда вызывается sort_by_key. Замыкание в Листинге 13-9 работает с sort_by_key, потому что оно только захватывает изменяемую ссылку на счетчик num_sort_operations и поэтому может быть вызван более одного раза.

Filename: 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
    );
}

Листинг 13-9: Использование замыкания FnMut с sort_by_key допускается.

Трейты Fn важны при определении или использовании функций или типов, которые используют замыкания. В следующем разделе мы обсудим итераторы. Многие методы итераторов принимают аргументы-замыкания, поэтому помните об этих деталях замыканий, когда мы продолжаем!

Резюме

Поздравляем! Вы завершили лабораторную работу по теме "Замыкания:анонимные функции, которые захватывают свою окружающую среду". Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.