Замыкания в Rust
Замыкание (closure) — это маленькая безымянная функция, которую можно сохранить в переменную и передавать дальше. От обычных функций замыкания отличаются тем, что умеют «захватывать» переменные из той области видимости, где они созданы.
Синтаксис замыкания
Заголовок раздела «Синтаксис замыкания»fn main() { let add_one = |x: i32| x + 1; println!("{}", add_one(5));}Вывод:
6Вертикальные черты |x: i32| заменяют круглые скобки, которые были бы у обычной функции. Сравни их рядом:
fn add_one_fn(x: i32) -> i32 { x + 1 } // обычная функцияlet add_one_closure = |x: i32| x + 1; // то же самое в виде замыканияТипы параметров и результата Rust часто выводит сам, поэтому замыкания нередко пишут ещё короче:
let add_one = |x| x + 1;У замыкания без параметров пустые черты: let say_hi = || println!("привет");, а вызывается оно как say_hi().
Замыкания в несколько строк
Заголовок раздела «Замыкания в несколько строк»Если выражений больше одного, оберни тело в фигурные скобки, как у функции:
fn main() { let describe = |n: i32| { let kind = if n % 2 == 0 { "чётное" } else { "нечётное" }; format!("{n} — {kind}") };
println!("{}", describe(7));}Вывод:
7 — нечётноеformat! работает как println!, но не печатает текст, а возвращает его в виде String. Здесь эта String — последнее выражение в скобках, поэтому замыкание её и возвращает.
Захват переменных из окружающей области видимости
Заголовок раздела «Захват переменных из окружающей области видимости»Вот чего нет у обычной функции: замыкание может пользоваться переменными из того места, где его определили, и передавать их явно не нужно:
fn main() { let discount = 10; let apply_discount = |price: i32| price - discount;
println!("{}", apply_discount(100)); // 90 println!("{}", apply_discount(50)); // 40}Вывод:
9040discount — не параметр замыкания: эта переменная просто лежит в области видимости main, а замыкание дотягивается до неё и использует напрямую. Обычная fn так не умеет — ей пришлось бы передать discount аргументом.
Замыкания, которые меняют захваченные переменные
Заголовок раздела «Замыкания, которые меняют захваченные переменные»Замыкание может и изменять захваченную переменную, если она объявлена с mut. Есть одно дополнительное правило: само замыкание тоже нужно сохранить в mut-переменную. Его вызов что-то меняет, и Rust хочет, чтобы это было видно в коде.
fn main() { let mut count = 0;
let mut increment = || { count += 1; println!("теперь count = {count}"); };
increment(); increment(); increment();
println!("итоговый count: {count}");}Вывод:
теперь count = 1теперь count = 2теперь count = 3итоговый count: 3Если забыть mut у increment, компилятор точно скажет, в чём дело: calling increment requires mutable binding due to mutable borrow of count («вызов increment требует изменяемой привязки, потому что count заимствуется изменяемо»).
Есть подвох, о котором стоит знать. Пока замыкание ещё может быть вызвано, у него есть право менять count, поэтому больше никто не может трогать count. Если добавить println!("{count}"); между двумя вызовами increment(), получишь error[E0502]: cannot borrow count as immutable because it is also borrowed as mutable («нельзя заимствовать count неизменяемо, потому что она уже заимствована изменяемо»). После последнего вызова increment() замыкание своё отработало, и count снова полностью твоя. Поэтому строка с итоговым count и работает. Вся история этого правила — в уроке Заимствование.
Иногда замыканию нужно полностью завладеть тем, что оно захватывает, — например, когда его передают в другой поток. Для этого перед чертами пишут move: move || { ... }. Как это работает на деле, ты увидишь в уроке Многопоточность.
Где замыкания встречаются на практике
Заголовок раздела «Где замыкания встречаются на практике»Сами по себе замыкания нужны нечасто. По-настоящему полезны они там, где в другую функцию нужно передать своё поведение, — особенно в методы итераторов, с которыми ты познакомишься в уроке Итераторы:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}Вывод:
[2, 4, 6, 8, 10]Здесь |n| n * 2 — замыкание, переданное прямо в .map(). Именно в таком виде (маленькое замыкание управляет методом вроде .map() или .filter()) ты и будешь использовать их каждый день.
Практика
Заголовок раздела «Практика»1. Цены с налогом
Заголовок раздела «1. Цены с налогом»Заведи переменную tax_percent, равную 20. Затем напиши замыкание with_tax, которое принимает цену и возвращает цену с налогом, используя захваченную tax_percent. Выведи результат для цен 50 и 120. Подсказка: налог — это price * tax_percent / 100.
Показать решение
fn main() { let tax_percent = 20; let with_tax = |price: i32| price + price * tax_percent / 100;
println!("50 с налогом — это {}", with_tax(50)); println!("120 с налогом — это {}", with_tax(120));}Вывод:
50 с налогом — это 60120 с налогом — это 144tax_percent нигде не передаётся. Замыкание захватывает её из main — ровно то, чего не смогла бы обычная fn.
2. Генератор ID
Заголовок раздела «2. Генератор ID»Напиши замыкание next_id, которое при первом вызове возвращает 1, при втором — 2, при третьем — 3 и так далее. Вызови его три раза и выведи каждый ID. Подсказка: держи mut-счётчик снаружи замыкания, прибавляй к нему 1 внутри и сделай счётчик последним выражением замыкания, чтобы он возвращался.
Показать решение
fn main() { let mut last_id = 0;
let mut next_id = || { last_id += 1; last_id };
println!("первый пользователь получает id {}", next_id()); println!("второй пользователь получает id {}", next_id()); println!("третий пользователь получает id {}", next_id());}Вывод:
первый пользователь получает id 1второй пользователь получает id 2третий пользователь получает id 3mut нужен и last_id, и next_id: первой — потому что меняется её значение, второму — потому что вызов замыкания меняет то, что оно захватило.
- Замыкание — это маленькая анонимная функция:
|параметры| выражение. - Типы часто выводятся автоматически, поэтому замыкания обычно короче аналогичной
fn. - В отличие от обычной функции, замыкание может захватывать переменные из области видимости, где его определили.
- Замыкание, которое меняет захваченную переменную, нужно хранить в
mut-переменной, и оно заимствует эту переменную, пока используется. - Полезнее всего замыкания там, где их передают в другие функции, — особенно в методы итераторов вроде
.map().
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Какие символы использует замыкание для параметров вместо круглых скобок функции?
2. Что умеет замыкание, чего не умеет обычная fn?
3. Где, судя по этой странице, замыкания чаще всего используются на практике?
4. Замыкание делает count += 1 с захваченной переменной. Что ещё нужно?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- Замыкания — книга «Язык программирования Rust»: официальная глава о замыканиях, выводе типов и трейтах
Fn(перевод на русский). - Захват ссылок или передача владения — книга «Язык программирования Rust»: три способа, которыми замыкание может захватить переменную, и когда нужен
move(перевод на русский). - Замыкания — Rust на примерах: короткие запускаемые примеры, где замыкания сравниваются с функциями (перевод на русский).
- Захват — Rust на примерах: ещё примеры замыканий, которые читают, меняют переменные и забирают владение ими (перевод на русский).
- Closure expressions — The Rust Reference (на английском): точные правила синтаксиса замыканий и захвата.