Функция как аргумент в Rust: от новичка до профи
Вы когда-нибудь писали на Python или JavaScript и с лёгкостью передавали функцию в другую функцию? А потом перешли на Rust и споткнулись на ровном месте? Знакомо. Вопрос "How do you pass a Rust function as a parameter?" — один из самых популярных на Stack Overflow. И не зря: система типов и владения Rust добавляет несколько слоёв сложности, но и открывает новые возможности.
В этой статье мы разберём три основных подхода: указатели на функции (fn), замыкания (closures) и трейты. Вы узнаете, когда какой использовать, и перестанете бояться функционального стиля в Rust.
Почему это важно?
Умение передавать функции как параметры — это не просто академический трюк. Это основа для:
- Создания гибких и переиспользуемых библиотек (например, итераторы, обработчики событий);
- Реализации паттернов "Стратегия" и "Наблюдатель";
- Написания лаконичного и выразительного кода без кучи if-else.
В Rust, в отличие от динамических языков, компилятор строго проверяет типы и время жизни. Поэтому нужно чётко понимать, что вы передаёте: просто адрес функции, замыкание с захватом контекста или что-то более сложное.
Подход 1: Указатель на функцию (Function Pointer) — fn
Самый простой и прямолинейный способ. Если ваша функция не захватывает переменные из внешней среды (она "чистая"), можно использовать тип fn. Обратите внимание: fn — это тип, а Fn, FnMut, FnOnce — это трейты. Не путайте!
fn add_one(x: i32) -> i32 {
x + 1
}
// Функция, принимающая другую функцию
fn apply_twice(f: fn(i32) -> i32, value: i32) -> i32 {
f(f(value))
}
fn main() {
let result = apply_twice(add_one, 5);
println!("Результат: {}", result); // 7
}
Когда использовать? Когда функция известна на этапе компиляции, не захватывает переменные и вам не нужна производительность замыканий. Указатели на функции — это zero-cost abstraction, они не требуют аллокаций.
Ограничения fn
Главный минус: вы не можете передать замыкание, которое захватывает переменные. Следующий код не скомпилируется:
fn main() {
let y = 10;
// Замыкание захватывает y
let closure = |x: i32| -> i32 { x + y };
// Ошибка! expected fn item, found closure
// let result = apply_twice(closure, 5);
}
Для этого нужны трейты Fn, FnMut, FnOnce.
Подход 2: Замыкания и трейты Fn, FnMut, FnOnce
Rust — язык системный, и он должен знать, что происходит с памятью. Замыкания могут захватывать переменные тремя способами:
- По ссылке (&T) — трейт
Fn. Можно вызывать сколько угодно раз, не меняя захваченные данные. - По изменяемой ссылке (&mut T) — трейт
FnMut. Можно вызывать много раз, но данные могут изменяться. - По значению (T) — трейт
FnOnce. Можно вызвать только один раз, после этого замыкание "потребляется".
Пример с Fn
fn apply_twice_fn<F>(f: F, value: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(value))
}
fn main() {
let y = 10;
let closure = |x| x + y; // захватывает y по ссылке
let result = apply_twice_fn(closure, 5);
println!("Fn: {}", result); // 25
}
Пример с FnMut
fn apply_twice_mut<F>(mut f: F, value: i32) -> i32
where
F: FnMut(i32) -> i32,
{
f(f(value))
}
fn main() {
let mut counter = 0;
let mut closure = |x: i32| -> i32 {
counter += 1;
x + counter
};
let result = apply_twice_mut(closure, 5);
println!("FnMut: {}", result); // 7 (5+1 + (5+1)+1 = 6+1 = 7? Нет, считаем: 5+1=6, 6+2=8)
// На самом деле: первый вызов: counter=1, возвращает 6. Второй вызов: counter=2, возвращает 6+2=8.
}
Пример с FnOnce
fn consume<F>(f: F, value: i32) -> i32
where
F: FnOnce(i32) -> i32,
{
f(value) // можно вызвать только один раз
}
fn main() {
let s = String::from("hello");
let closure = |x: i32| -> i32 {
println!("{}", s); // захватывает s по значению
x
};
let result = consume(closure, 42);
// closure больше нельзя использовать
println!("FnOnce: {}", result);
}
Подход 3: Трейт-объекты и динамическая диспетчеризация
Иногда вам нужно хранить коллекцию функций или передавать их в структуру. В этом случае удобно использовать Box<dyn Fn(i32) -> i32>. Это добавляет небольшие накладные расходы (виртуальная таблица), но даёт гибкость.
struct Processor {
handler: Box<dyn Fn(i32) -> i32>,
}
impl Processor {
fn new<F>(f: F) -> Self
where
F: Fn(i32) -> i32 + 'static,
{
Processor {
handler: Box::new(f),
}
}
fn process(&self, value: i32) -> i32 {
(self.handler)(value)
}
}
fn main() {
let multiplier = |x: i32| -> i32 { x * 2 };
let proc = Processor::new(multiplier);
println!("Динамическая диспетчеризация: {}", proc.process(10)); // 20
}
Важно: 'static в трейт-объекте означает, что замыкание не содержит ссылок на временные данные. Если вам нужно передать ссылки, используйте Box<dyn Fn(i32) -> i32 + '_>.
Сравнение подходов: что выбрать?
БлогиРекомендаций по книгамЧитайте также |
|---|