Jak przekazać funkcję Rust jako parametr?

89

Czy mogę przekazać funkcję jako parametr? Jeśli nie, jaka jest dobra alternatywa?

Próbowałem różnych składni, ale nie znalazłem właściwej. Wiem, że potrafię to zrobić:

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

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

ale to nie jest przekazywanie funkcji jako parametru do innej funkcji:

fn fun_test(value: i32, (some_function_prototype)) -> i32 {
    println!("{}", value);
    value
}
Anioł Anioł
źródło

Odpowiedzi:

124

Oczywiście że możesz:

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);
}

Ponieważ jest to Rust, musisz wziąć pod uwagę własność i czas życia zamknięcia .

TL; DR; Zasadniczo istnieją 3 typy domknięć (obiekty wywoływalne):

  1. Fn: Nie może modyfikować przechwyconych obiektów.
  2. FnMut: Może modyfikować przechwycone obiekty.
  3. FnOnce: Najbardziej ograniczone. Można go wywołać tylko raz, ponieważ gdy jest wywoływany, konsumuje siebie i łapie.

Zobacz Kiedy zamknięcie implementuje Fn, FnMut i FnOnce? po więcej szczegółów

Jeśli używasz prostego wskaźnika do funkcji, takiego jak zamknięcie, zbiór przechwytywania jest pusty i masz Fnsmak.

Jeśli chcesz robić bardziej wymyślne rzeczy, będziesz musiał użyć funkcji lambda.

W Rust znajdują się odpowiednie wskaźniki do funkcji, które działają tak samo jak w C. Ich typ to na przykład fn(i32) -> i32. Pliki Fn(i32) -> i32, FnMut(i32) -> i32iFnOnce(i32) -> i32 są w rzeczywistości cechy. Wskaźnik do funkcji zawsze implementuje wszystkie trzy z nich, ale Rust ma również domknięcia, które mogą, ale nie muszą, być konwertowane na wskaźniki (w zależności od tego, czy zbiór przechwytywania jest pusty) do funkcji, ale implementują one niektóre z tych cech.

Na przykład przykład z góry można rozwinąć:

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
}
rodrigo
źródło
1
jest różnica w używaniu <F: Fn ...> lub nie (.., f: & Fn ...) te dwie prace, jakiś szczegół, który muszę znać?
Angel Angel
@AngelAngel: No, Fn*to cechy, więc zwykle <T: Trait>vs (t: &T)dotyczy. Głównym ograniczeniem rozwiązania innego niż rodzajowy jest to, że musi być używane z odniesieniami. Więc jeśli chcesz FnOnce, który powinien zostać przekazany jako kopia, musisz użyć stylu ogólnego.
rodrigo
5
Zwróć uwagę, że bardziej idiomatyczne jest użycie typów ogólnych zamiast obiektów cech (tj. <F: Fn..>Zamiast (f: &Fn...). Jest to powód - typy generyczne spowodują wysyłanie statyczne, podczas gdy obiekty cech wymagają dynamicznej wysyłki.
Vladimir Matveev
3
Co ciekawe, z punktu widzenia interfejsu (dzwoniącego) FnOncejest to w rzeczywistości najbardziej ogólna cecha - akceptuje wszystkie zamknięcia, niezależnie od tego, czy odczytują, modyfikują, czy przejmują własność stanu przechwyconego. FnMutjest bardziej restrykcyjny, nie akceptuje domknięć, które przejmują własność przechwyconego obiektu (ale nadal umożliwia modyfikacje stanu). Fnjest najbardziej restrykcyjny, ponieważ nie akceptuje zamknięć, które modyfikują ich stan przechwycenia. Zatem wymaganie &Fnnakłada największe ograniczenie na funTestdzwoniącego, a jednocześnie zapewnia najmniejsze ograniczenie tego, jak fmożna w nim wywołać.
użytkownik4815162342
30

Fn, FnMutI FnOnceprzedstawił w drugiej odpowiedzi, są zapinane na typy. Rodzaje funkcji, które zamykają się w ich zakresie.

Oprócz przekazywania zamknięć Rust obsługuje również przekazywanie prostych (niezamykających) funkcji, takich jak:

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) -> i32tutaj jest typ wskaźnika funkcji .

Jeśli nie potrzebujesz pełnoprawnego zamknięcia, praca z typami funkcji jest często prostsza, ponieważ nie musi zajmować się tymi niuansami dotyczącymi czasu życia zamknięcia.

ArtemGr
źródło
czy to zadziała z metodami structa?
Ivan Temchenko
@IvanTemchenko Może? Oto kod do gry: play.rust-lang.org/…
ArtemGr
nie do końca to miałem na myśli =) Znalazłem obejście, aby zwrócić zamknięcie dyn, które przechwytuje stan jaźni, więc nie muszę omijać referencji instancji ...
Ivan Temchenko