Kapitel 07 - Funktionen & Closures: Deine Backstube und die magischen Miniköche
Willkommen in deiner Programmier-Backstube! Bis jetzt haben wir in Rust gelernt, wie man Variablen erstellt, Daten speichert und Entscheidungen trifft. Aber wenn wir immer mehr Code schreiben, wird unser Programm schnell unübersichtlich. Stell dir vor, du müsstest jedes Mal, wenn du einen Kuchen backen willst, die komplette Anleitung von vorne aufschreiben. Das wäre extrem anstrengend!
In diesem Kapitel lernen wir zwei mächtige Werkzeuge kennen, die uns das Leben leichter machen:
- Funktionen – unsere festen Backrezepte.
- Closures – unsere magischen Miniköche, die sich flexibel anpassen können.
1. Was ist eine Funktion? (Die Analogie des Backrezepts)
Eine Funktion ist im Grunde nichts anderes als ein festes Backrezept, das an einer zentralen Stelle in deinem Backbuch steht. Jedes Mal, wenn du diesen bestimmten Kuchen backen möchtest, schlägst du einfach das Rezept auf und rufst: “Ofen an, backe Kuchen!”
Ein Rezept hat meistens drei Teile:
- Die Zutaten (Eingaben / Parameter): Was stecken wir in die Funktion hinein? (Zum Beispiel: Mehl, Eier, Zucker).
- Die Zubereitung (Der Rumpf der Funktion): Was passiert in der Küche? (Der Teig wird gerührt, der Ofen heizt).
- Das Ergebnis (Die Ausgabe / Rückgabewert): Was kommt am Ende heraus? (Ein leckerer Schokoladenkuchen).
So sieht ein Rezept in Rust aus
Lass uns ein echtes Backrezept in Rust-Code schreiben. Wir wollen eine Funktion bauen, die aus zwei Zutaten (Mehl in Gramm und Eier als Anzahl) einen Teig mischt.
// Das ist unser Backrezept (die Funktion)
fn mache_teig(mehl_gramm: i32, anzahl_eier: i32) -> String {
println!("Mische {}g Mehl mit {} Eiern...", mehl_gramm, anzahl_eier);
// Das ist das fertige Ergebnis, das wir zurückgeben
let ergebnis = String::from("Ein klebriger Kuchenteig");
ergebnis
}
fn main() {
println!("Starten wir unsere Backstube!");
// Hier rufen wir das Rezept auf und geben die Zutaten hinein
let mein_teig = mache_teig(500, 4);
println!("In unserer Schüssel liegt jetzt: {}", mein_teig);
}
Lass uns den Code Zeile für Zeile unter die Lupe nehmen:
fn mache_teig(...): Mit dem Wörtchenfn(kurz für function) sagen wir Rust: “Achtung, jetzt definiere ich ein neues Rezept!” Danach folgt der Name der Funktion:mache_teig. Wir schreiben Funktionsnamen in Rust immer in Kleinbuchstaben mit Unterstrichen (snake_case).mehl_gramm: i32, anzahl_eier: i32: Das sind unsere Parameter (die Zutaten). In Rust müssen wir bei Funktionen immer ganz genau sagen, welchen Datentyp die Zutaten haben.i32bedeutet eine ganze Zahl. Rust ist hier sehr streng, damit in der Küche nichts schiefgehen kann (wir wollen ja keine Schrauben statt Eier in den Teig werfen!).-> String: Der Pfeil->zeigt uns, was am Ende aus dem Ofen herauskommt (der Rückgabetyp). In diesem Fall gibt unsere Funktion einen Text (String) zurück.- Die geschweiften Klammern
{ ... }: Sie bilden den Arbeitsbereich unserer Küche (den Funktionskörper). Alles, was hier drin steht, wird ausgeführt, wenn wir die Funktion aufrufen. let mein_teig = mache_teig(500, 4);: In dermain-Funktion rufen wir unser Rezept auf. Wir übergeben die konkreten Werte500und4(das nennt man Argumente) und fangen das fertige Ergebnis in der Variablenmein_teigauf.
2. Der Semikolon-Trick: Ausdrücke vs. Anweisungen
Hast du dich in unserem Beispiel oben gewundert, warum in der Zeile ergebnis kein Semikolon ; am Ende steht? Das ist kein Tippfehler, sondern einer der wichtigsten Tricks in Rust!
Rust unterscheidet ganz streng zwischen zwei Dingen:
- Anweisungen (Statements): Sie tun etwas, geben aber nichts zurück. Sie enden immer mit einem Semikolon
;. Stell dir vor, du stellst eine Schüssel auf den Tisch. Das ist eine Aktion, aber es kommt kein neuer Wert dabei heraus. - Ausdrücke (Expressions): Sie berechnen einen Wert und geben ihn zurück. Sie haben kein Semikolon
;am Ende. Stell dir vor, du reichst jemandem den fertigen Kuchen.
Die Analogie des Stoppschilds
- Ein Semikolon
;wirkt wie ein Stoppschild für Werte. Es sagt Rust: “Führe diese Aktion aus, aber wirf den Wert danach weg!” - Wenn du das Semikolon in der letzten Zeile einer Funktion weglässt, wird diese Zeile zu einem Ausdruck. Rust nimmt das Ergebnis dieser Zeile und wirft es automatisch aus der Funktion heraus – direkt zu demjenigen, der die Funktion aufgerufen hat.
Lass uns das an einem ganz einfachen Beispiel anschauen:
#![allow(unused)]
fn main() {
fn addiere_fünf(zahl: i32) -> i32 {
zahl + 5 // KEIN Semikolon! Das bedeutet: Gib das Ergebnis von (zahl + 5) zurück.
}
}
Was passiert, wenn wir aus Versehen ein Semikolon setzen?
#![allow(unused)]
fn main() {
// ACHTUNG: Das wird einen Compilerfehler erzeugen!
fn addiere_fünf_fehlerhaft(zahl: i32) -> i32 {
zahl + 5; // HIER steht ein Semikolon!
}
}
Wenn du diesen Code kompilieren willst, schimpft der Rust-Compiler sofort mit dir:
error[E0308]: mismatched types
--> src/main.rs:1:38
|
1 | fn addiere_fünf_fehlerhaft(zahl: i32) -> i32 {
| ----------------------- ^^^ expected `i32`, found `()`
2 | zahl + 5;
| - help: remove this semicolon to return this value
Was will uns der Compiler damit sagen?
Durch das Semikolon am Ende von zahl + 5; hast du den Rückgabewert blockiert. Rust denkt nun, die Funktion gibt gar nichts zurück (den sogenannten Unit-Typ (), was man sich wie eine leere Schachtel vorstellen kann). Oben in der Signatur (-> i32) hast du aber versprochen, eine Zahl zurückzugeben. Der Compiler merkt, dass das Versprechen gebrochen wurde, und gibt dir direkt den Tipp: “Entferne dieses Semikolon, um diesen Wert zurückzugeben!”
3. Closures: Die magischen Miniköche
Jetzt wird es richtig spannend! Neben den festen Backrezepten (Funktionen) gibt es in Rust noch Closures (sprich: “Kloschurs”).
Eine Closure ist wie ein anonymer Minikoch, den du direkt an deiner Arbeitsplatte einstellst. Dieser Koch hat keinen festen Namen im Backbuch (deshalb nennt man sie auch anonyme Funktionen), aber er kann blitzschnell Aufgaben für dich erledigen.
Das Besondere an unserem Minikoch: Er kann sich einfach Zutaten schnappen, die schon auf der Arbeitsplatte herumstehen, selbst wenn sie gar nicht offiziell als Parameter an ihn übergeben wurden! Diesen Vorgang nennt man Capturing (Einfangen der Umgebung).
Die Syntax des Minikochs
Stell dir vor, die Parameter einer Closure sind wie die Hände des Kochs. Statt runden Klammern () benutzen wir bei Closures zwei gerade Striche || (das sieht ein bisschen aus wie ein kleiner Kühlergrill oder zwei Kochlöffel).
Hier ist ein einfaches Beispiel:
fn main() {
// 1. Eine normale Variable auf unserer Küchenzeile
let extra_zucker = 50;
// 2. Wir definieren unseren Minikoch (die Closure)
// Er nimmt eine Zutat (mehl) entgegen und schnappt sich heimlich den extra_zucker!
let minikoch = |mehl: i32| {
println!("Ich mische {}g Mehl...", mehl);
println!("Und ich nehme mir heimlich {}g Zucker von der Arbeitsplatte!", extra_zucker);
mehl + extra_zucker
};
// 3. Wir lassen den Minikoch arbeiten
let gesamtgewicht = minikoch(200);
println!("Das Gesamtgewicht der Zutaten ist: {}g", gesamtgewicht);
}
Siehst du, wie der minikoch auf die Variable extra_zucker zugreifen konnte, obwohl wir sie ihm gar nicht beim Aufruf übergeben haben? Eine normale Funktion fn darf das niemals! Eine Funktion darf nur benutzen, was man ihr direkt als Argument hineinreicht. Der Minikoch (die Closure) dagegen hat ein gutes Gedächmisse und merkt sich die Umgebung, in der er erschaffen wurde.
4. Die drei Arten von Miniköchen (Die Essens-Analogie)
Weil Rust extrem vorsichtig mit dem Speicher deines Computers umgeht, muss der Compiler genau wissen, wie ein Minikoch mit den Zutaten aus der Umgebung umgeht. Es gibt drei Arten von Zugriffen, und Rust hat für jede Art einen eigenen Fachbegriff (einen sogenannten Trait).
Wir können uns diese drei Typen hervorragend mit einer Kühlschrank- und Essens-Analogie merken!
graph TD
A[Die 3 Closure-Typen] --> B[Fn: Nur Gucken]
A --> C[FnMut: Topf verrühren]
A --> D[FnOnce: Aufessen]
B --> B1["Kühlschrank ansehen<br>(Lesezugriff / &T)"]
C --> C1["Zutaten verändern<br>(Schreibzugriff / &mut T)"]
D --> D1["Zutat komplett essen<br>(Ownership / T)"]
1. Fn – Der “Gucker” (Nur ansehen)
Analogie: Der Minikoch macht die Kühlschranktür auf und schaut sich die Zutaten an. Er nimmt nichts heraus, er verändert nichts, er guckt einfach nur. Weil sich nichts ändert, können auch andere Köche gleichzeitig in den Kühlschrank schauen.
- In Rust: Das ist ein Lesezugriff (
&T). Die Umgebung wird nur ausgeliehen. - Häufigkeit: Da dies der friedlichste Zugriff ist, kann diese Closure beliebig oft aufgerufen werden.
fn main() {
let rezept_name = String::from("Apfelkuchen");
// Der Minikoch liest nur die Variable 'rezept_name'
let zeige_rezept = || {
println!("Ich lese das Rezept für: {}", rezept_name);
};
// Wir können ihn mehrmals aufrufen!
zeige_rezept();
zeige_rezept();
// Die Variable 'rezept_name' ist danach immer noch da und benutzbar
println!("Wir lieben {}", rezept_name);
}
2. FnMut – Der “Rührer” (Verändern / Mutable)
Analogie: Der Minikoch nimmt einen Kochlöffel und verrührt die Zutaten im Topf. Er fügt Gewürze hinzu und verändert den Zustand des Essens. Die Zutaten bleiben in der Küche, aber sie sehen danach anders aus als vorher.
- In Rust: Das ist ein veränderbarer Lesezugriff (
&mut T). Die Closure verändert Variablen aus ihrer Umgebung. - Wichtig: Weil sich Dinge ändern, muss die Closure selbst als veränderbar (
mut) markiert werden.
fn main() {
let mut anzahl_kekse = 10;
// Der Minikoch verändert 'anzahl_kekse' direkt auf der Arbeitsplatte
// Weil er etwas verändert, müssen wir 'mut keks_dieb' schreiben!
let mut keks_dieb = || {
anzahl_kekse -= 1; // Ein Keks wird stibitzt!
println!("Mampf! Es sind nur noch {} Kekse da.", anzahl_kekse);
};
keks_dieb();
keks_dieb();
// Am Ende hat sich der Wert der Originalvariable verändert:
println!("In der Keksbox sind am Ende: {} Kekse.", anzahl_kekse); // 8
}
3. FnOnce – Der “Vielfraß” (Aufessen)
Analogie: Der Minikoch schnappt sich eine exklusive, seltene Zutat (zum Beispiel eine goldene Erdbeere) und isst sie komplett auf. Die Erdbeere ist danach weg! Sie existiert nicht mehr. Weil die Zutat weg ist, kann der Koch dieses Rezept nur ein einziges Mal ausführen. Wenn er es ein zweites Mal versuchen würde, gäbe es keine Erdbeere mehr zum Essen.
- In Rust: Die Closure übernimmt den Besitz (Ownership) der Variable (
T). - Wichtig: Diese Closure kann nur ein einziges Mal aufgerufen werden (daher der Name
Once= einmal).
fn main() {
// Eine Zutat, die nicht kopiert werden kann (ein String auf dem Heap)
let seltene_erdbeere = String::from("Goldene Erdbeere");
// Der Minikoch verbraucht die Erdbeere (er nimmt das Ownership)
// Das 'move'-Schlüsselwort zwingt die Closure dazu, die Zutat komplett einzusacken.
let erdbeer_esser = move || {
println!("Ich esse die {} auf! Mmh, lecker!", seltene_erdbeere);
// Hier endet das Leben der seltenen Erdbeere, sie wird zerstört (dropped)
};
// Wir rufen die Closure auf
erdbeer_esser();
// Wenn wir versuchen würden, 'erdbeer_esser()' ein zweites Mal aufzurufen,
// würde uns Rust einen Fehler melden, da die Erdbeere bereits gegessen wurde!
// Auch hier können wir nicht mehr auf die Erdbeere zugreifen:
// println!("{}", seltene_erdbeere); // FEHLER! Erdbeere existiert nicht mehr.
}
5. Typische Stolpersteine und Compilerfehler
Der Rust-Compiler ist wie ein sehr genauer Küchenchef. Er passt auf, dass kein Chaos entsteht. Lass uns zwei typische Fehler anschauen, die Anfängern oft passieren, und lernen, wie wir sie beheben.
Fehler 1: Der doppelte Diebstahl (FnOnce mehrfach aufrufen)
Stell dir vor, du versuchst, den “Vielfraß”-Koch zweimal nacheinander essen zu lassen:
// ACHTUNG: Dieser Code kompiliert nicht!
fn main() {
let zutat = String::from("Schokolade");
let koch = move || {
let _aufgegessen = zutat; // Hier wandert die Zutat in den Koch
println!("Schokolade gegessen!");
};
koch();
koch(); // FEHLER! Wir rufen den Koch ein zweites Mal auf
}
Der Compiler wird dir folgendes sagen:
error[E0382]: use of moved value: `koch`
--> src/main.rs:11:5
|
10 | koch();
| ------ `koch` moved due to this call
11 | koch();
| ^^^^ value used here after move
Die Lösung:
Wenn eine Closure Ownership übernimmt (durch move oder weil sie die Variable im Inneren verbraucht), darfst du sie nicht mehrmals aufrufen. Wenn du den Code mehrmals ausführen willst, darfst du die Zutat im Inneren nicht aufbrauchen, sondern solltest sie nur als Referenz (&zutat) ausleihen!
Fehler 2: Das vergessene Semikolon bei Funktionen ohne Rückgabe
Manchmal schreiben wir eine funktion, die einfach nur etwas auf dem Bildschirm ausgeben soll, setzen aber aus Versehen am Ende keinen Wert oder bauen verwirrende Semikolons ein:
#![allow(unused)]
fn main() {
// Was ist hier falsch?
fn begruessung() -> String {
println!("Hallo in der Backstube!");
// Huch, wo ist der Rückgabewert?
}
}
Hier hast du versprochen, einen String zurückzugeben (-> String), hast aber gar keinen String am Ende der Funktion hingeschrieben.
Die Lösung:
Entweder entfernst du das -> String, weil die Funktion gar nichts zurückgeben muss:
#![allow(unused)]
fn main() {
fn begruessung() { // Kein Pfeil nötig!
println!("Hallo in der Backstube!");
}
}
Oder du gibst tatsächlich einen String zurück:
#![allow(unused)]
fn main() {
fn begruessung() -> String {
println!("Hallo in der Backstube!");
String::from("Hallo!") // Ohne Semikolon!
}
}
Zusammenfassung für deine Kochmütze
- Funktionen (
fn) sind wie feste, beschriftete Rezepte im Backbuch. Sie können keine Variablen aus ihrer Umgebung einfach so mopsen. - Ausdrücke (ohne
;) geben Werte zurück; Anweisungen (mit;) tun nur etwas und blockieren die Rückgabe. - Closures (
|| {}) sind Miniköche auf Abruf, die sich Variablen von der Arbeitsplatte schnappen können. - Es gibt drei Closure-Typen:
Fn: Schaut sich die Zutaten nur an (Lesezugriff).FnMut: Verrührt und verändert die Zutaten (Schreibzugriff).FnOnce: Isst die Zutaten komplett auf (Besitz/Ownership wird verbraucht, nur 1x ausführbar).
Herzlichen Glückwunsch! Du hast jetzt das Rüstzeug, um deine eigenen Programme modular und übersichtlich zu gestalten. Schnapp dir deine Kochschürze und probiere die Übungen im nächsten Abschnitt aus!