Замыкания в Rust
Замыкание в Rust — это анонимная функция, которая может захватывать и хранить переменные из своего окружения.
Замыкания позволяют обращаться к переменным вне области видимости, в которой они определены, и при необходимости могут перемещать или заимствовать их в замыкание.
Замыкания широко используются в Rust в таких областях, как функциональное программирование, конкурентное программирование и событийно-ориентированное программирование.
Различия между замыканиями и функциями
И замыкания, и обычные функции могут инкапсулировать часть логики, но у замыканий есть дополнительная ключевая возможность: захват переменных из внешней среды. В следующей таблице перечислены основные различия между ними:
| Характеристика | Замыкание | Функция |
|---|---|---|
| Анонимность | Нет имени; обычно присваивается переменной | Имеет фиксированноеfnname |
| Захват окружения | Может захватывать внешние переменные | Не может захватывать внешние переменные |
| Синтаксис определения | |parameters| expression | fn name(parameters) |
| Вывод типов | Типы параметров и возвращаемых значений обычно можно опускать | Необходимо явно указывать типы параметров и возвращаемых значений |
| Хранение и передача | Можно использовать как переменную, параметр или возвращаемое значение | Также поддерживается |
Объявление и вызов замыканий
Базовый синтаксис замыкания выглядит следующим образом:
let closure_name = |参数列表| 表达式或语句块;
Параметры могут иметь аннотации типов или их можно опустить — компилятор Rust выводит их из контекста.
Пример
// Замыкание: аннотации типов опущены, компилятор выводит автоматически
let add_one = |x| x + 1;
println!("add_one(4) = {}", add_one(4)); // Вывод: 5
// Замыкание: явно указать типы параметров и возвращаемого значения
let multiply = |a: i32, b: i32| -> i32 { a * b };
println!("multiply(3, 7) = {}", multiply(3, 7)); // Вывод: 21
// Многострочное замыкание требует фигурных скобок
let greet = |name: &str| {
let greeting = format!("Hello, {}!", name);
greeting
};
println!("{}", greet("example")); // Вывод: Hello, example!
}
Замыкания вызываются точно так же, как обычные функции: добавьте круглые скобки после имени переменной и передайте аргументы.
Захват внешних переменных
Самая важная характеристика замыкания — это его способность захватывать переменные из области видимости, в которой оно определено. Это фундаментальное отличие замыканий от обычных функций.
Замыкания могут захватывать внешние переменные тремя способами:
| Способ захвата | Соответствующая семантика Rust | Описание |
|---|---|---|
| Захват по ссылке | Похоже на&T | Поведение по умолчанию; замыкание заимствует переменную, и она остаётся доступной снаружи |
| Захват по изменяемому заимствованию | Похоже на&mut T | Замыканию нужно изменять переменную; само замыкание должно быть объявлено какmut |
| Захват по значению | Похоже наT | Используйтеmoveключевое слово, перемещающее владение переменной в замыкание |
Для типов, которые реализуют
Copyтрейт (например,i32、bool),moveсоздаётся только копия значения, и внешняя переменная остаётся доступной. Поэтому при демонстрации передачи владения выбирайтеString、Vecи другие не-Copyтипы.
Захват по ссылке
По умолчанию замыкание заимствует внешние переменные по неизменяемой ссылке. После окончания заимствования внешняя область видимости может продолжать использовать переменную.
Пример
let text = String::from("example");
// Замыкание захватывает text по ссылке и не забирает владение
let print_text = || println!("text = {}", text);
print_text(); // Вывод: text = example
// Заимствование завершено; text всё ещё можно использовать снаружи
println!("Всё ещё можно использовать снаружи: {}", text);
}
Захват по изменяемой ссылке
Если замыканию нужно изменить захваченную переменную, Rust захватывает её по изменяемой ссылке. В этом случае само замыкание должно быть объявлено какmut。
Пример
let mut counter = 0;
// Замыкание захватывает counter через &mut; само замыкание также должно быть объявлено как mut
let mut inc = || {
counter += 1;
};
inc();
inc();
println!("counter = {}", counter); // Вывод: counter = 2
}
Захват по значению (move)
Добавив ключевое словоmoveперед замыканием, замыкание получает владение захваченными переменными. После передачи владения внешняя область больше не может использовать переменную.
Пример
let owned = String::from("EXAMPLE");
// move передаёт владение owned в замыкание
let take_owned = move || println!("owned = {}", owned);
take_owned(); // Вывод: owned = EXAMPLE
// Если раскомментировать строку ниже, компиляция завершится ошибкой: владение owned было перемещено в замыкание
// println!("{}", owned);
}
Когда замыкание нужно передать в другой поток или вернуть за пределы его области видимости,moveэто особенно часто используется — это гарантирует, что замыкание владеет необходимыми данными, и предотвращает висячие ссылки.
Трейты замыканий: Fn / FnMut / FnOnce
Компилятор Rust автоматически реализует соответствующий трейт для замыкания в зависимости от того, как оно захватывает переменные. Понимание этих трёх трейтов — ключ к использованию замыканий в качестве параметров функций или возвращаемых значений.
| Трейт | Способ захвата | Количество вызовов | Типичный сценарий |
|---|---|---|---|
Fn | Неизменяемое заимствование (&T) | Несколько раз | Доступ к захваченным переменным только для чтения |
FnMut | Изменяемое заимствование (&mut T) | Несколько раз | Требуется изменять захваченные переменные |
FnOnce | Забирает владение (T) | Только один раз | Потребляет захваченные переменные; после этого его нельзя вызвать снова |
Отношение наследования между этими тремя таково:FnДаFnMutявляется подтрейтом,FnMutДаFnOnceявляется подтрейтом. То есть замыкание, которое реализуетFnавтоматически также реализуетFnMutиFnOnce。
Примечание:
moveКлючевое слово лишь заставляет захватить владение; это не означает, что замыкание обязательно должно бытьFnOnce. Если тело замыкания не потребляет захваченное значение, даже еслиmoveдобавлено, замыкание всё ещё может реализовыватьFn(можно вызывать несколько раз).
Пример
let name = String::from("example");
// Замыкание Fn: только читает, не изменяет и может вызываться несколько раз
let greet = || println!("Hello, {}!", name);
greet(); // Первый вызов
greet(); // Второй вызов, всё ещё работает нормально
// Замыкание FnMut: должно изменять захваченные переменные
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("increment() = {}", increment()); // Вывод: 1
println!("increment() = {}", increment()); // Вывод: 2
// Замыкание FnOnce: потребляет захваченные переменные, может быть вызвано только один раз
let data = String::from("EXAMPLE");
let consume = move || {
let _ = data; // Перемещает data из окружения замыкания, потребляя её
println!("данные были потреблены");
};
consume();
// consume(); // Если раскомментировать, компиляция завершится ошибкой: замыкания FnOnce можно вызвать только один раз
}
Замыкания как параметры и возвращаемые значения
Замыкания можно передавать как параметры функции и возвращать как возвращаемые значения функции. Это два наиболее распространённых способа использования замыканий в реальной разработке.
Замыкания как параметры
При использовании замыкания в качестве параметра необходимо указать тип трейта замыкания с помощью обобщённого ограничения. В следующем примере параметрFограниченFn(i32) -> i32, то есть замыкание, которое принимаетi32параметр и возвращаетi32значение.
Пример
fn apply<F>(val: i32, f: F) -> i32
где
F: Fn(i32) -> i32, // F должен реализовывать Fn(i32) -> i32
{
f(val)
}
fn main() {
let double = |x| x * 2;
let result = apply(5, double);
println!("Result: {}", result); // Вывод: Result: 10
// Вы также можете передать анонимное замыкание напрямую
let result2 = apply(3, |x| x + 100);
println!("Result2: {}", result2); // Вывод: Result2: 103
}
Замыкания как возвращаемые значения
Поскольку тип замыкания анонимен, при возврате замыкания требуетсяimpl TraitилиBox<dyn Trait>описать возвращаемый тип.
Возврат замыкания с impl Fn
Когда тип возвращаемого замыкания может быть определён во время компиляции, используйтеimpl Fnи выделение памяти в куче не требуется.
Пример
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// Нужно использовать move; иначе x — локальная ссылка, и после возврата замыкания x становится недействительным
move |y| x + y
}
fn main() {
let add_five = make_adder(5);
println!("5 + 3 = {}", add_five(3)); // Вывод: 5 + 3 = 8
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // Вывод: 10 + 2 = 12
}
Используйте Box<dyn Fn> для возврата замыканий
Когда нужно динамически выбирать разные замыкания для возврата во время выполнения, используйтеBox<dyn Fn>Разместите замыкание в куче.
Пример
Box::new(move |y| x + y)
}
fn main() {
let add_ten = make_adder(10);
println!("10 + 2 = {}", add_ten(2)); // Вывод: 10 + 2 = 12
}
| Метод | Место выделения | Применимые сценарии |
|---|---|---|
impl Fn | Стек | Тип замыкания может быть определён во время компиляции, что обеспечивает лучшую производительность |
Box<dyn Fn> | Куча | Динамически выбирать замыкания во время выполнения или требуется хранить замыкания между функциями |
Типичные случаи использования
Замыкания повсеместно используются в повседневной разработке на Rust. Вот несколько наиболее типичных сценариев применения.
Замыкания в итераторах
Замыкания часто используются вместе с методами итераторов для пакетной обработки элементов коллекции.
Пример
let nums = vec![1, 2, 3, 4, 5];
// map: преобразовать каждый элемент
let squared: Vec<i32> = nums.iter().map(|x| x * x).collect();
println!("squares: {:?}", squared); // Вывод: [1, 4, 9, 16, 25]
// filter: отфильтровать элементы, удовлетворяющие условию
let even: Vec<&i32> = nums.iter().filter(|x| *x % 2 == 0).collect();
println!("evens: {:?}", even); // Вывод: [2, 4]
// fold: свернуть коллекцию в одно значение
let sum: i32 = nums.iter().fold(0, |acc, x| acc + x);
println!("sum: {}", sum); // Вывод: 15
}
Замыкания и многопоточность
В многопоточном программировании замыкания часто используются для определения исполняемого тела потока.moveКлючевое слово `move` в этом сценарии почти обязательно, поскольку оно перемещает владение данными в новый поток, не позволяя ссылкам между потоками стать недействительными.
Пример
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// Создать поток для каждого числа; move передаёт владение num в поток
let handles: Vec<_> = nums.into_iter().map(|num| {
thread::spawn(move || {
num * 2
})
}).collect();
// Дождаться завершения всех потоков и собрать результаты
for handle in handles {
let result = handle.join().unwrap();
println!("Result: {}", result);
}
}
Замыкания и обработка ошибок
Замыкания могут возвращатьResultилиOptionтипы, которые в сочетании с методами итераторов реализуют лаконичную логику обработки ошибок.
Пример
let nums = vec![3, -1, 4, -5, 9];
// Использовать замыкание для поиска первого положительного числа
let first_positive = nums.iter().find(|&&x| x > 0);
match first_positive {
Some(&n) => println!("First positive number: {}", n), // Вывод: First positive number: 3
None => println!("No positive numbers"),
}
// Используйте замыкание для фильтрации и преобразования в сочетании с Result для обработки возможных ошибок
let results: Vec<Result<i32, &str>> = nums.iter().map(|&n| {
if n > 0 {
ОК(n * 10)
} else {
Err("Отрицательные числа не могут быть обработаны")
}
}).collect();
for r in results {
совпадение r {
Ok(v) => println!("Успех: {}", v),
Err(e) => println!("Ошибка: {}", e),
}
}
}
Производительность и времена жизни
Производительность замыканий
Замыкания Rust легковесны. Компилятор выполняет оптимизации встраивания для замыканий, благодаря чему накладные расходы вызова замыканий близки к прямому вызову обычных функций. Сами замыкания не вносят дополнительных вызовов виртуальных функций или выделений памяти в куче (если только явно не используетсяBox)。
Замыкания и времена жизни
Время жизни замыкания тесно связано с переменными, которые оно захватывает. Система времён жизни Rust гарантирует, что замыкание не переживёт ни одну переменную, которую оно захватывает — если замыкание ссылается на локальную переменную, компилятор не позволит вернуть замыкание за пределы области видимости этой переменной во время компиляции.
Пример
let text = String::from("example");
// Правильно: используйте замыкание в пределах области видимости text
let print_text = || println!("{}", text);
print_text(); // Вывод: example
// Если вы попытаетесь вернуть это замыкание, компилятор сообщит об ошибке:
// fn make_closure() -> impl Fn() {
// let text = String::from("example");
// || println!("{}", text) // Ошибка: время жизни text недостаточно велико
// }
// Решение: используйте move, чтобы передать владение в замыкание
}
Полный пример
В следующем примере всесторонне демонстрируются основные способы использования замыканий, такие как объявление, захват внешних переменных и передача в качестве аргументов.
Пример
fn apply_operation<F>(num: i32, operation: F) -> i32
где
F: Fn(i32) -> i32,
{
operation(num)
}
fn main() {
let num = 5;
// Определить замыкание: возвести число в квадрат
let square = |x| x * x;
// Передать замыкание в функцию в качестве аргумента
let result = apply_operation(num, square);
println!("Квадрат числа {} равен {}", num, result); // Вывод: Квадрат числа 5 равен 25
// Вы также можете передать анонимное замыкание напрямую
let result2 = apply_operation(num, |x| x * x * x);
println!("Куб числа {} равен {}", num, result2); // Вывод: Куб числа 5 равен 125
}
Запустите программу; вывод будет следующим:
Square of 5 is 25 Cube of 5 is 125
Итог
Замыкания в Rust — это мощная абстракция, которая предоставляет гибкий и выразительный способ инкапсуляции логики.
Замыкания могут захватывать переменные окружения и могут передаваться как параметры или возвращаться как возвращаемые значения. В сочетании с итераторами замыкания позволяют удобно реализовывать сложные задачи обработки данных.
Дизайн замыканий в Rust учитывает безопасность, производительность и время жизни — компилятор гарантирует на этапе компиляции, что замыкания не ссылаются на недействительные переменные, и обеспечивает нулевую стоимость абстракции для вызовов замыканий за счёт оптимизации встраивания.
Другие расширения