Wie übergibt man eine Rust-Funktion als Parameter?

81

Kann ich eine Funktion als Parameter übergeben? Wenn nicht, was ist eine gute Alternative?

Ich habe verschiedene Syntaxen ausprobiert, aber nicht die richtige gefunden. Ich weiß ich kann das:

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

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

Damit wird die Funktion jedoch nicht als Parameter an eine andere Funktion übergeben:

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

Antworten:

111

Sicher kannst du:

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

Da dies Rust ist, müssen Sie das Eigentum und die Lebensdauer der Schließung berücksichtigen .

TL; DR; Grundsätzlich gibt es 3 Arten von Verschlüssen (aufrufbare Objekte):

  1. Fn: Die erfassten Objekte können nicht geändert werden.
  2. FnMut: Es kann die erfassten Objekte ändern.
  3. FnOnce: Am meisten eingeschränkt. Kann nur einmal aufgerufen werden, da es sich selbst und seine Erfassungen verbraucht, wenn es aufgerufen wird.

Siehe Wann implementiert ein Abschluss Fn, FnMut und FnOnce? für mehr Details

Wenn Sie einen einfachen Zeiger auf eine Funktion wie das Schließen verwenden, ist das Erfassungsset leer und Sie haben den FnGeschmack.

Wenn Sie mehr ausgefallene Sachen machen wollen, müssen Sie Lambda-Funktionen verwenden.

In Rust gibt es richtige Zeiger auf Funktionen, die genau wie in C funktionieren. Ihr Typ ist zum Beispiel fn(i32) -> i32. Die Fn(i32) -> i32, FnMut(i32) -> i32und FnOnce(i32) -> i32sind tatsächlich Züge. Ein Zeiger auf eine Funktion implementiert immer alle drei, aber Rust hat auch Verschlüsse, die in Zeiger (abhängig davon, ob der Erfassungssatz leer ist) auf Funktionen konvertiert werden können oder nicht, aber einige dieser Merkmale implementieren.

So kann beispielsweise das Beispiel von oben erweitert werden:

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
quelle
1
Es gibt einen Unterschied bei der Verwendung von <F: Fn ...> oder nicht (.., f: & Fn ...) der beiden Werke, einige Details, die ich wissen muss?
Angel Angel
@AngelAngel: Nun, Fn*sind Züge, so die übliche <T: Trait>vs (t: &T)gilt. Die Hauptbeschränkung der nicht generischen Lösung besteht darin, dass sie mit Referenzen verwendet werden muss. Wenn Sie also möchten FnOnce, was als Kopie übergeben werden soll, müssen Sie den generischen Stil verwenden.
Rodrigo
5
Beachten Sie, dass es idiomatischer ist, Generika anstelle von Merkmalsobjekten zu verwenden (dh <F: Fn..>anstelle von (f: &Fn...). Und dies aus einem Grund - Generika führen zu einem statischen Versand, während Merkmalsobjekte einen dynamischen Versand erfordern.
Vladimir Matveev
3
Interessanterweise ist aus Sicht der Schnittstelle (Anrufer) FnOncedas allgemeinste Merkmal - es akzeptiert alle Schließungen, unabhängig davon, ob sie den erfassten Status lesen, ändern oder in Besitz nehmen. FnMutist restriktiver, akzeptiert keine Schließungen, die den Besitz eines erfassten Objekts übernehmen (erlaubt jedoch weiterhin Statusänderungen). Fnist am restriktivsten, da es keine Schließungen akzeptiert, die ihren erfassten Status ändern. Das Erfordernis &Fnstellt also die größte Einschränkung für den funTestAnrufer dar, während die geringste Einschränkung für die Art und Weise festgelegt wird, wie fin ihm aufgerufen werden kann.
user4815162342
29

Fn, FnMutUnd FnOncein der anderen Antwort dargelegt, sind Verschlusstypen. Die Arten von Funktionen, die sich über ihren Umfang schließen.

Neben dem Übergeben von Verschlüssen unterstützt Rust auch das Übergeben einfacher (nicht schließender) Funktionen wie folgt:

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) -> i32Hier ist ein Funktionszeigertyp .

Wenn Sie keinen vollwertigen Verschluss benötigen, ist die Arbeit mit Funktionstypen oft einfacher, da Sie sich nicht mit diesen Verschlusslebensdauern befassen müssen.

ArtemGr
quelle