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 20-28 gezeigt, wo wir eine
Funktion add_one
definiert haben, die ihrem Parameter 1 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 20-28: 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 Methode map
zu verwenden, um einen Vektor von
Zahlen in einen Vektor von Zeichenketten zu verwandeln, könnten wir einen
Funktionsabschluss verwenden, wie in Codeblock 20-29.
#![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(); }
Codeblock 20-29: Verwendung eines Funktionsabschlusses
mit der Methode map
zur Umwandlung von Zahlen in Zeichenketten
Oder wir könnten eine Funktion als Argument für map
angeben anstelle des
Funktionsabschlusses. Codeblock 20-30 zeigt, wie das aussehen würde.
#![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(); }
Codeblock 20-30: Verwenden der Methode
String::to_string
zur Umwandlung von Zahlen in Zeichenketten
Beachte, dass wir die vollständig qualifizierte Syntax verwenden müssen, über
die wir iin „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 „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 nehmen, wie in Codeblock 20-32 zu sehen ist.
#![allow(unused)] fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
Codeblock 20-31: Verwenden eines
Aufzählungs-Initialisierers mit der Methode map
zum Erstellen einer
Status
-Instanz aus Zahlen
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 normalerweise nicht tun, weil sie
keinen konkreten Typ haben, den man zurückgeben kann. Es ist dir beispielsweise
nicht erlaubt, den Funktionszeiger fn
als Rückgabetyp zu verwenden, wenn der
Funktionsabschluss irgendwelche Werte aus seinem Gültigkeitsbereich erfasst.
Stattdessen wirst du normalerweise die Syntax impl Trait
verwenden, die wir
in Kapitel 10 kennengelernt haben. Du kannst jeden Funktionstyp zurückgeben,
indem du Fn
, FnOnce
und FnMut
verwendest. Zum Beispiel wird der Code in
Codeblock 20-32 problemlos funktionieren.
#![allow(unused)] fn main() { fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } }
Codeblock 20-32: Rückgeben eines Funktionsabschlusses aus
einer Funktion unter Verwendung der Syntax impl Trait
Wie wir jedoch in „Funktionsabschluss-Typinferenz und Annotation“ in Kapitel 13 festgestellt haben, ist jeder Funktionsabschluss auch ein eigener Typ. Wenn du mit mehreren Funktionen arbeiten musst, die dieselbe Signatur, aber unterschiedliche Implementierungen haben, musst du ein Merkmals-Objekt für sie verwenden. Überlege, was passiert, wenn du einen Code wie in Codeblock 20-33 schreibst.
Dateiname: src/main.rs
fn main() { let handlers = vec![returns_closure(), returns_initialized_closure(123)]; for handler in handlers { let output = handler(5); println!("{output}"); } } fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 { move |x| x + init }
Codeblock 20-33: Erstellen eines Vec<T>
von
Funktionsabschlüssen, die durch Funktionen definiert sind, die impl Fn
zurückgeben
Hier haben wir zwei Funktionen returns_closure
und
returns_initialized_closure
, die beide impl Fn(i32) -> i32
zurückgeben. Man
beachte, dass die Funktionsabschlüsse, die sie zurückgeben, unterschiedlich
sind, obwohl sie den gleichen Typ implementieren. Wenn wir versuchen, dies zu
kompilieren, lässt uns Rust wissen, dass es nicht funktionieren wird:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:4:9
|
4 | returns_initialized_closure(123)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
12 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
16 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:12:25>)
found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:16:46>)
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
Die Fehlermeldung sagt uns, dass Rust jedes Mal, wenn wir ein impl Trait
zurückgeben, einen eindeutigen undurchsichtigen Typ (opaque type) erzeugt,
einen Typ, bei dem wir nicht in die Details dessen sehen können, was Rust für
uns konstruiert. Obwohl diese Funktionen also beide Funktionsabschlüsse
zurückgeben, die dasselbe Merkmal implementieren, nämlich Fn(i32) -> i32
,
sind die undurchsichtigen Typen, die Rust für jede Funktion erzeugt,
unterschiedlich. (Dies ist vergleichbar mit der Art und Weise, wie Rust
unterschiedliche konkrete Typen für verschiedene asynchrone Blöcke erzeugt,
selbst wenn sie denselben Ausgabetyp haben, wie wir in „Arbeiten mit einer
beliebigen Anzahl von Futures“ in Kapitel 17 gesehen
haben. Eine Lösung für dieses Problem haben wir jetzt schon ein paar Mal
gesehen: Wir können ein Merkmals-Objekt verwenden, wie in Codeblock 20-34.
fn main() { let handlers = vec![returns_closure(), returns_initialized_closure(123)]; for handler in handlers { let output = handler(5); println!("{output}"); } } fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> { Box::new(move |x| x + init) }
Codeblock 20-34: Erstellen eines Vec<T>
von
Funktionsabschlüssen, die durch Funktionen definiert sind, die Box<dyn Fn>
zurückgeben, damit sie denselben Typ haben
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 18.
Als nächstes wollen wir uns Makros ansehen!