Avatar ·

Как передать функцию в качестве параметра в Rust?

Всем привет! Я только начал изучать Rust и столкнулся с проблемой. В других языках, например в Python или JavaScript, я могу легко передать функцию как аргумент в другую функцию. Например: ```python def apply(func, x): return func(x) def double(x): return x * 2 result = apply(double, 5) ``` В Rust я попытался сделать что-то подобное, но компилятор выдаёт ошибки. Вот мой код: ```rust fn apply(func: ???, x: i32) -> i32 { func(x) } fn double(x: i32) -> i32 { x * 2 } fn main() { let result = apply(double, 5); println!("{}", result); } ``` Я не понимаю, какой тип указать для параметра `func`. Пробовал писать `fn(i32) -> i32`, но это не сработало. В документации написано про трейты `Fn`, `FnMut` и `FnOnce`, но я запутался, какой из них использовать и как правильно объявлять такие функции. Подскажите, как правильно передать функцию как параметр в Rust? Желательно с наглядными примерами. <<>>

Использование трейтов Fn, FnMut и FnOnce для передачи функций

В Rust функции передаются через трейты-замыкания: Fn, FnMut и FnOnce. Они позволяют гибко работать как с обычными функциями, так и с замыканиями (closures). Вот правильное решение вашей задачи:

fn apply<F>(func: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    func(x)
}

fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    let result = apply(double, 5);
    println!("{}", result); // Выведет: 10
    
    // Также можно передать замыкание:
    let triple = |x: i32| x * 3;
    let result2 = apply(triple, 5);
    println!("{}", result2); // Выведет: 15
}

Разбор решения

  • Обобщённый параметр F — позволяет принимать любой тип, реализующий нужный трейт.
  • Fn(i32) -> i32 — трейт, означающий, что функция может быть вызвана многократно без изменения состояния. Подходит для обычных функций и замыканий, которые не захватывают изменяемые переменные.
  • where-ограничение — явно указывает, что тип F должен реализовывать трейт Fn.

Разница между Fn, FnMut и FnOnce

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

  • Fn — функция может быть вызвана сколько угодно раз, не изменяя захваченные данные. Используйте по умолчанию.
  • FnMut — функция может изменять захваченные переменные (мутабельная ссылка). Пример:
fn apply_mut<F>(mut func: F, x: i32) -> i32
where
    F: FnMut(i32) -> i32,
{
    func(x)
}

let mut multiplier = 2;
let mut closure = |x: i32| x * multiplier;
multiplier = 3; // Ошибка! closure уже захватил multiplier
// Правильнее:
let mut closure = |x: i32| x * multiplier;
apply_mut(closure, 5);
  • FnOnce — функция может быть вызвана только один раз. Используется, когда замыкание потребляет захваченные значения (перемещает их).
fn apply_once<F>(func: F, x: i32) -> i32
where
    F: FnOnce(i32) -> i32,
{
    func(x)
}

let name = String::from("Hello");
let closure = |x: i32| {
    println!("{}", name); // name перемещается в замыкание
    x + 1
};
apply_once(closure, 5); // Работает
// closure(); // Ошибка: closure уже перемещён

Практический совет

Если вы не уверены, какой трейт использовать, начинайте с Fn. Компилятор подскажет, если нужно FnMut или FnOnce. Для обычных функций (не замыканий) всегда подходит Fn.

Полный пример с разными типами функций

fn main() {
    // Обычная функция
    fn add_one(x: i32) -> i32 { x + 1 }
    println!("{}", apply(add_one, 10)); // 11
    
    // Замыкание без захвата
    let add_two = |x: i32| x + 2;
    println!("{}", apply(add_two, 10)); // 12
    
    // Срез как аргумент (функция, принимающая &str)
    fn process<F>(f: F, s: &str) where F: Fn(&str) -> String {
        println!("{}", f(s));
    }
    process(|s| format!("Hello, {}!", s), "World");
}
<<>>

Альтернативный подход: указатели на функции (fn pointer)

Если вам не нужны замыкания и вы работаете только с обычными функциями (не closures), можно использовать тип fn(i32) -> i32 — это указатель на функцию. Он проще, но менее гибкий:

fn apply(func: fn(i32) -> i32, x: i32) -> i32 {
    func(x)
}

fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    let result = apply(double, 5);
    println!("{}", result); // 10
    
    // Замыкания без захвата тоже можно передать, но не все:
    let triple = |x: i32| x * 3;
    // apply(triple, 5); // Ошибка! Замыкание — не fn pointer
}

Когда использовать fn pointer?

  • Когда нужно передать только именованные функции (не замыкания).
  • Для совместимости с C-кодом (extern "C" fn).
  • Когда важна минимальная сложность кода.

Недостатки

Указатели на функции не работают с замыканиями, которые захватывают переменные. Также они не поддерживают мутабельное состояние. В большинстве случаев рекомендуется использовать обобщённый подход с трейтами Fn, FnMut или FnOnce.

Сравнение

// fn pointer — работает только с обычными функциями
fn with_fn_ptr(f: fn(i32) -> i32, x: i32) -> i32 { f(x) }

// Fn — работает с функциями и замыканиями (без захвата мутабельных данных)
fn with_fn<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }

// Пример использования
fn square(x: i32) -> i32 { x * x }
let closure = |x: i32| x + 10;

println!("{}", with_fn_ptr(square, 5)); // 25
// println!("{}", with_fn_ptr(closure, 5)); // Ошибка!
println!("{}", with_fn(closure, 5)); // 15