Erweiterte Funktionen und Funktionsabschlüsse (closures)

Dieser Abschnitt befasst sich mit fortgeschrittenen Funktionalitäten im Zusammenhang mit Funktionen und Funktionsabschlüsse, einschließlich Funktionszeigern und Zurückgeben von Funktionsabschlüssen.

Funktionszeiger

Wir haben darüber gesprochen, wie man Funktionsabschlüsse an Funktionen übergibt; man kann auch reguläre Funktionen an Funktionen übergeben! Diese Technik ist nützlich, wenn du eine Funktion, die du bereits definiert hast, übergeben willst, anstatt einen neuen Funktionsabschluss zu definieren. Funktionen haben den Typ fn (mit kleinem f), nicht zu verwechseln mit dem Funktionsabschlussmerkmal (closure trait) Fn. Der Typ fn wird Funktionszeiger (function pointer) genannt. Die Übergabe von Funktionen mit Funktionszeigern ermöglicht es dir, Funktionen als Argumente für andere Funktionen zu verwenden.

Die Syntax für die Angabe, dass ein Parameter ein Funktionszeiger ist, ähnelt der von Funktionsabschlüssen, wie in Codeblock 19-27 gezeigt, wo wir eine Funktion add_one definiert haben, die ihrem Parameter eins hinzufügt. Die Funktion do_twice nimmt zwei Parameter entgegen: Einen Funktionszeiger auf eine beliebige Funktion mit einem i32-Parameter und einem i32-Rückgabewert, und einen i32-Parameter. Die Funktion do_twice ruft die Funktion f zweimal auf, übergibt ihr den Wert arg und addiert dann die Ergebnisse der beiden Funktionsaufrufe zusammen. Die Funktion main ruft do_twice mit den Argumenten add_one und 5 auf.

Dateiname: src/main.rs

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("Die Antwort ist: {answer}");
}

Codeblock 19-27: Verwenden des Typs fn zum Entgegennehmen eines Funktionszeigers als Argument

Dieser Code gibt Die Antwort ist: 12 aus. Wir spezifizieren, dass der Parameter f in do_twice ein fn ist, das einen Parameter vom Typ i32 nimmt und ein i32 zurückgibt. Wir können dann f im Rumpf von do_twice aufrufen. In main können wir den Funktionsnamen add_one als erstes Argument an do_twice übergeben.

Im Gegensatz zu Funktionsabschlüssen ist fn ein Typ, nicht ein Merkmal, daher spezifizieren wir fn direkt als Parametertyp, anstatt einen generischen Typparameter mit einem Merkmal Fn als Merkmalsabgrenzung (trait bound) zu deklarieren.

Funktionszeiger implementieren alle drei Funktionsabschlussmerkmale (Fn, FnMut und FnOnce), was bedeutet, dass du immer einen Funktionszeiger als Argument an eine Funktion übergeben kannst, die einen Funktionsabschluss erwartet. Es ist am besten, Funktionen mit einem generischen Typ und einer der Funktionsabschlussmerkmale zu schreiben, sodass deine Funktionen entweder Funktionen oder Funktionsabschlüsse akzeptieren können.

Ein Beispiel, bei dem du nur fn und keine Funktionsabschlüsse akzeptieren möchtest, ist die Schnittstelle zu externem Code, der keine Funktionsabschlüsse hat: C-Funktionen können Funktionen als Argumente akzeptieren, aber C hat keine Funktionsabschlüsse.

Als Beispiel dafür, wo du entweder einen inline definierten Funktionsabschluss oder eine benannte Funktion verwenden könntest, sehen wir uns die Verwendung der Methode map an, die vom Merkmal Iterator in der Standardbibliothek bereitgestellt wird. Um die Funktion map zu verwenden, um einen Vektor von Zahlen in einen Vektor von Zeichenketten zu verwandeln, könnten wir einen Funktionsabschluss wie diesen verwenden:

#![allow(unused)]
fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

Oder wir könnten eine Funktion als Argument für map angeben anstelle des Funktionsabschlusses, so wie hier:

#![allow(unused)]
fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

Beachte, dass wir die vollständig qualifizierte Syntax verwenden müssen, über die wir vorhin im Abschnitt „Fortgeschrittene Merkmale (traits)“ gesprochen haben, weil es mehrere Funktionen namens to_string gibt. Hier verwenden wir die Funktion to_string, die im Merkmal ToString definiert ist, welche die Standardbibliothek für jeden Typ implementiert hat, der Display implementiert.

Aus dem Abschnitt „Werte in Aufzählungen“ in Kapitel 6 wissen wir, dass der Name jeder definierten Aufzählungsvariante auch eine Initialisierungsfunktion ist. Wir können diese Initialisierungsfunktionen als Funktionszeiger verwenden, die die Funktionsabschlussmerkmale implementieren, was bedeutet, dass wir die Initialisierungsfunktionen als Argumente für Methoden angeben können, die Funktionsabschlüsse wie diese nehmen:

#![allow(unused)]
fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

Hier erzeugen wir Status::Value-Instanzen für die u32-Werte im Bereich, für den map aufgerufen wird, indem wir die Initialisierungsfunktion von Status::Value verwenden. Einige Leute bevorzugen diesen Stil und einige Leute ziehen es vor, Funktionsabschlüsse zu verwenden. Sie kompilieren zum gleichen Code, also verwende den Stil, der für dich am klarsten ist.

Zurückgeben von Funktionsabschlüssen

Funktionsabschlüsse werden durch Merkmale repräsentiert, was bedeutet, dass du Funktionsabschlüsse nicht direkt zurückgeben kannst. In den meisten Fällen, in denen du ein Merkmal zurückgeben möchtest, kannst du stattdessen den konkreten Typ, der das Merkmal implementiert, als Rückgabewert der Funktion verwenden. Aber das kannst du bei Funktionsabschlüssen nicht tun, weil sie keinen konkreten Typ haben, der rückgabefähig ist; es ist dir beispielsweise nicht erlaubt, den Funktionszeiger fn als Rückgabetyp zu verwenden.

Der folgende Code versucht, einen Funktionsabschluss direkt zurückzugeben, aber er lässt sich nicht kompilieren:

#![allow(unused)]
fn main() {
fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}
}

Der Kompilierfehler ist folgender:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0746]: return type cannot have an unboxed trait object
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
help: return an `impl Trait` instead of a `dyn Trait`, if all returned values are the same type
  |
1 | fn returns_closure() -> impl Fn(i32) -> i32 {
  |                         ~~~~
help: box the return type, and wrap all of the returned values in `Box::new`
  |
1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
2 ~     Box::new(|x| x + 1)
  |

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` (lib) due to 1 previous error

Der Fehler bezieht sich wieder auf das Merkmal Sized! Rust weiß nicht, wie viel Platz benötigt wird, um den Funktionsabschluss zu speichern. Wir haben vorhin eine Lösung für dieses Problem gesehen. Wir können ein Merkmalsobjekt (trait object) verwenden:

#![allow(unused)]
fn main() {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
}

Dieser Code lässt sich sehr gut kompilieren. Weitere Informationen über Merkmalsobjekte findest du im Abschnitt „Merkmalsobjekte (trait objects) die Werte unterschiedlicher Typen erlauben“ in Kapitel 17.

Als nächstes wollen wir uns Makros ansehen!