Як ви передаєте функцію Rust як параметр?


89

Чи можу я передати функцію як параметр? Якщо ні, то яка хороша альтернатива?

Я спробував кілька синтаксисів, але не знайшов правильного. Я знаю, що можу це зробити:

fn example() {
    let fun: fn(value: i32) -> i32;
    fun = fun_test;
    fun(5i32);
}

fn fun_test(value: i32) -> i32 {
    println!("{}", value);
    value
}

але це не передає функцію як параметр іншій функції:

fn fun_test(value: i32, (some_function_prototype)) -> i32 {
    println!("{}", value);
    value
}

Відповіді:


123

Звичайно, ви можете:

fn fun_test(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}

fn times2(value: i32) -> i32 {
    2 * value
}

fn main() {
    fun_test(5, &times2);
}

Оскільки це Руст, ви повинні взяти до уваги право власності та термін дії закриття .

TL; DR; В основному існує 3 типи закриття (об’єкти, що викликаються):

  1. Fn: Він не може змінювати об'єкти, які він захоплює.
  2. FnMut: Він може змінювати об'єкти, які він захоплює.
  3. FnOnce: Найбільш обмежений. Можна викликати лише один раз, тому що коли він викликаний, він поглинає себе і захоплює.

Див. Коли закриття реалізує Fn, FnMut та FnOnce? для більш детальної інформації

Якщо ви використовуєте простий вказівник на функцію, наприклад, закриття, тоді набір захоплення порожній, і у вас є Fnсмак.

Якщо ви хочете зробити більше вигадливих речей, то вам доведеться використовувати лямбда-функції.

У Rust є відповідні вказівники на функції, які працюють так само, як і в C. Їх тип, наприклад fn(i32) -> i32. Те Fn(i32) -> i32, FnMut(i32) -> i32і FnOnce(i32) -> i32насправді є рисами. Вказівник на функцію завжди реалізує всі три з них, але у Rust також є замикання, які можуть бути перетворені в покажчики (залежно від того, чи є набір захоплення порожнім) у функції, а можуть і не, але вони реалізують деякі з цих рис.

Так, наприклад, приклад зверху можна розширити:

fn fun_test_impl(value: i32, f: impl Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}
fn fun_test_dyn(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}
fn fun_test_ptr(value: i32, f: fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}

fn times2(value: i32) -> i32 {
    2 * value
}

fn main() {
    let y = 2;
    //static dispatch
    fun_test_impl(5, times2);
    fun_test_impl(5, |x| 2*x);
    fun_test_impl(5, |x| y*x);
    //dynamic dispatch
    fun_test_dyn(5, &times2);
    fun_test_dyn(5, &|x| 2*x);
    fun_test_dyn(5, &|x| y*x);
    //C-like pointer to function
    fun_test_ptr(5, times2);
    fun_test_ptr(5, |x| 2*x); //ok: empty capture set
    fun_test_ptr(5, |x| y*x); //error: expected fn pointer, found closure
}

1
є різниця у використанні <F: Fn ...> чи ні (.., f: & Fn ...) цих двох робіт, якась деталь, яку мені потрібно знати?
Angel Angel

@AngelAngel: Ну, Fn*це риси, тому застосовується звичайне <T: Trait>проти (t: &T). Основне обмеження негенеричного рішення полягає в тому, що воно повинно використовуватися з посиланнями. Отже, якщо ви хочете FnOnce, що має бути передано як копію, ви повинні використовувати загальний стиль.
rodrigo

5
Зауважте, що більш ідіоматично використовувати дженерики замість об’єктів ознак (тобто <F: Fn..>замість (f: &Fn...). І це з певної причини - дженерики призведуть до статичної відправки, тоді як об’єкти ознак потребують динамічної відправки.
Володимир Матвєєв

3
Цікаво, що з точки зору інтерфейсу (абонента) FnOnceнасправді є найбільш загальною рисою - вона приймає всі закриття, незалежно від того, читають вони, змінюють чи приймають право власності на захоплений стан. FnMutє більш обмежувальним, він не приймає закриття, яке приймає право власності на захоплений об'єкт (але все одно дозволяє змінити стан). Fnє найбільш обмежувальним, оскільки не приймає закриття, що змінює їх захоплений стан. Таким чином, вимагаючи &Fnвстановлює найбільше обмеження для funTestабонента, одночасно забезпечуючи найменше обмеження щодо того, як fможна викликати всередині нього.
користувач4815162342

30

Fn, FnMutі FnOnce, як зазначено в іншій відповіді, це типи закриття . Типи функцій, які закриваються за обсягом.

Окрім проходження закриття, Rust також підтримує передачу простих (незакритих) функцій, наприклад:

fn times2(value: i32) -> i32 {
    2 * value
}

fn fun_test(value: i32, f: fn(i32) -> i32) -> i32 {
    println!("{}", f (value));
    value
}

fn main() {
    fun_test (2, times2);
}

fn(i32) -> i32ось тип покажчика на функцію .

Якщо вам не потрібне повноцінне закриття, то робота з типами функцій часто простіша, оскільки вона не має справу з цими життєвими обмеженнями закриття.


чи буде це працювати з методами struct?
Іван Темченко

@IvanTemchenko Може? Ось деякий код, з яким можна пограти: play.rust-lang.org/…
ArtemGr

це не зовсім те, що я мав на увазі =) Знайшов обхідний шлях для повернення закриття dyn, який фіксує стан себе, тому мені не потрібно передавати інстанцію з посиланням ...
Іван Темченко
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.