Функции как аргументы в Rust: 3 способа передать функцию

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функция как аргумент в 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 + '_>.

Сравнение подходов: что выбрать?

Блоги

Рекомендаций по книгам