Перейти к содержанию

Что такое замыкания в Rust

Posted on:29 июня 2023 г. at 13:23

Example Dynamic OG Image link

Что такое замыкание?

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

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

let add_one = |x| x + 1;
println!("{}", add_one(5));  // вывод: 6

Здесь add_one является замыканием, которое принимает один аргумент, x, и возвращает x + 1.

Захват переменных

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

  1. By reference: &T
  2. По изменяемой ссылке: &mut T
  3. По значению: Т

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

let x = 7;

let borrow = || println!("borrow: {}", x);
let borrow_mut = || println!("borrow_mut: {}", x);
let move = move || println!("move: {}", x);

borrow();
borrow_mut();
move();

В этом примере заимствование захватывает x по ссылке (&T), borrow_mut захватывает x по изменяемой ссылке (&mut T) и перемещает захваченную переменную x по значению (T). Ключевое слово move используется для принудительной передачи в замыкание во владение значения, которые оно использует.

Вывод типа и дженерики

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

let example_closure = |x| x;

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

В этом коде example_closure можете использовать строку или i32, поскольку он является универсальным.

Fn, FnMut, и FnOnce

В Rust мы имеем три трейта для представления трех видов замыканий: Fn, FnMut и FnOnce, которые соответствуют трем способам захвата переменных:

  1. Fn принимает переменные по ссылке (&T)
  2. FnMut принимает переменные по изменяемой ссылке (&mut T)
  3. FnOnce принимает переменные по значению (T)

Каждое замыкание реализует один или несколько из этих трейтов. Если замыкание реализует Fn, то оно также реализует FnMut и FnOnce. Если оно реализует FnMut, оно также реализует FnOnce.

let x = 7;

let fn_closure = || println!("{}", x);  // Реализует Fn
let mut fnmut_closure = || println!("{}", x);  // Реализует FnMut
let fnonce_closure = move || println!("{}", x);  // Реализует FnOnce

Практическое использование замыканий

Существует множество способов использования замыканий в Rust для написания краткого и гибкого кода. Давайте обсудим некоторые из них:

В качестве аргументов функций

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

Вот пример, где мы используем замыкание для реализации функции map, подобной функции map на итераторах:

fn map<F>(value: i32, func: F) -> i32
where
    F: Fn(i32) -> i32,
{
    func(value)
}

let double = |x| x * 2;
println!("{}", map(5, double)); // Выводит: 10

В этом примере map принимает i32, а замыкание в качестве аргументов применяет замыкание к значению и возвращает результат.

Замыкания в структурах данных

Хранение замыкания в структуре для последующего выполнения является типичным шаблоном в Rust, особенно в конструкциях, управляемых событиями или обратными вызовами.

struct Button<F> {
    callback: F,
}

impl<F: Fn()> Button<F> {
    fn new(callback: F) -> Self {
        Self { callback }
    }

    fn click(&self) {
        (self.callback)();
    }
}

let button = Button::new(|| println!("Кнопка нажата!"));
button.click();  // Выводит: Кнопка нажата!

В этом примере создается структура Button, хранящая замыкание как обратный вызов. При нажатии кнопки вызывается обратный вызов.

Замыкания с итераторами

Замыкания широко используются с итераторами в Rust. Они обеспечивают краткий способ выполнения операций над каждым элементом сбора. Например:

let numbers = vec![1, 2, 3, 4, 5];
let squares: Vec<_> = numbers.iter().map(|x| x * x).collect();
println!("{:?}", squares);  // Выводит: [1, 4, 9, 16, 25]

В этом примере замыкание | x | x * x передается методу map итератора, который применяет замыкание к каждому элементу.

В заключение

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