Funktionen

Funktionen sind im Rust-Code weit verbreitet. Du hast bereits eine der wichtigsten Funktionen in der Sprache gesehen: Die Funktion main, die der Einstiegspunkt vieler Programme ist. Du hast auch das Schlüsselwort fn gesehen, mit dem du neue Funktionen deklarieren kannst.

Rust-Code verwendet die Schlangenschrift-Stil-Konvention (snake case) für Funktions- und Variablennamen, bei der alle Buchstaben klein geschrieben sind und Unterstriche Wörter separieren. Hier ist ein Programm, das eine Beispiel-Funktionsdefinition enthält:

Dateiname: src/main.rs

fn main() {
    println!("Hallo Welt!");

    another_function();
}

fn another_function() {
    println!("Eine andere Funktion.");
}

Wir definieren eine Funktion in Rust durch die Eingabe von fn, gefolgt von einem Funktionsnamen und einem Satz Klammern. Die geschweiften Klammern teilen dem Compiler mit, wo der Funktionsrumpf beginnt und endet.

Wir können jede Funktion, die wir definiert haben, aufrufen, indem wir ihren Namen gefolgt von einem Satz Klammern eingeben. Da another_function im Programm definiert ist, kann sie von innerhalb der main-Funktion aufgerufen werden. Beachte, dass wir another_function nach der main-Funktion im Quellcode definiert haben; wir hätten sie auch vorher definieren können. Rust interessiert es nicht, wo du deine Funktionen definierst, nur dass sie irgendwo definiert sind.

Lass uns ein neues Binärprojekt namens „functions“ anfangen, um Funktionen weiter zu erforschen. Platziere das Beispiel another_function in src/main.rs und lass es laufen. Du solltest die folgende Ausgabe sehen:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hallo Welt!
Eine andere Funktion.

Die Zeilen werden in der Reihenfolge ausgeführt, in der sie in der main-Funktion erscheinen. Zuerst wird die Nachricht „Hallo Welt!“ ausgegeben und dann wird another_function aufgerufen und ihre Nachricht ausgegeben.

Parameter

Wir können Funktionen auch so definieren, dass sie Parameter haben, das sind spezielle Variablen, die Teil der Funktionssignatur sind. Wenn eine Funktion Parameter hat, kannst du sie mit konkreten Werten für diese Parameter versehen. Technisch gesehen werden die konkreten Werte Argumente genannt, aber in lockeren Gesprächen neigen Leute dazu, die Worte Parameter und Argument entweder für die Variablen in der Definition einer Funktion oder für die konkreten Werte, die beim Aufruf einer Funktion übergeben werden, synonym zu verwenden.

In dieser Version von another_function fügen wir einen Parameter hinzu:

Dateiname: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("Der Wert von x ist: {x}");
}

Versuche, dieses Programm auszuführen; du solltest die folgende Ausgabe erhalten:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
Der Wert von x ist: 5

Die Deklaration another_function hat einen Parameter namens x. Der Typ von x wird als i32 angegeben. Wenn wir 5 an another_function übergeben, setzt das Makro println! den Wert 5 an die Stelle, an der sich das Paar geschweifter Klammern mit dem x darin in der Formatierungszeichenkette befand.

In Funktionssignaturen musst du den Typ jedes Parameters deklarieren. Dies ist eine bewusste Designentscheidung von Rust: Das Erfordernis von Typ-Annotationen in Funktionsdefinitionen bedeutet, dass der Compiler sie fast nie an anderer Stelle im Code benötigt, um herauszufinden, welchen Typ du meinst. Der Compiler ist auch in der Lage, hilfreichere Fehlermeldungen zu geben, wenn er weiß, welche Typen die Funktion erwartet.

Wenn wir mehrere Parameter definieren, trennen wir die Parameterdeklarationen mit Kommas, so wie hier:

Dateiname: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("Das Maß ist: {value}{unit_label}");
}

Dieses Beispiel erzeugt eine Funktion namens print_labeled_measurement mit zwei Parametern. Der erste Parameter heißt value und ist ein i32. Der zweite heißt unit_label und ist vom Typ char. Die Funktion gibt dann einen Text aus, der sowohl value als auch unit_label enthält.

Lass uns versuchen, diesen Code auszuführen. Ersetze das Programm, das sich derzeit in der Datei src/main.rs deines „functions“-Projekts befindet, durch das vorhergehende Beispiel und führe es mit cargo run aus:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
Das Maß ist: 5h

Da wir die Funktion mit 5 als Wert für value und 'h' als Wert für unit_label aufgerufen haben, enthält die Programmausgabe diese Werte.

Anweisungen und Ausdrücke

Funktionsrümpfe bestehen aus einer Reihe von Anweisungen, die optional mit einem Ausdruck enden können. Bisher haben wir nur Funktionen ohne einen endenden Ausdruck behandelt, aber du hast einen Ausdruck als Teil einer Anweisung gesehen. Da Rust eine auf Ausdrücken basierende Sprache ist, ist dies eine wichtige Unterscheidung, die es zu verstehen gilt. Andere Sprachen haben nicht dieselben Unterscheidungen, deshalb wollen wir uns ansehen, was Anweisungen und Ausdrücke sind und wie sich ihre Unterschiede auf die Funktionsrümpfe auswirken.

  • Anweisungen (statements) sind Instruktionen, die eine Aktion ausführen und keinen Wert zurückgeben.
  • Ausdrücke (expressions) werden zu einem Ergebniswert ausgewertet. Schauen wir uns einige Beispiele an.

Eine Variable zu erstellen und ihr mit dem Schlüsselwort let einen Wert zuzuweisen, ist eine Anweisung. In Codeblock 3-1 ist let y = 6; eine Anweisung.

Dateiname: src/main.rs

fn main() {
    let y = 6;
}

Codeblock 3-1: Eine Funktionsdeklaration main, die eine Anweisung enthält

Auch Funktionsdefinitionen sind Anweisungen; das gesamte vorhergehende Beispiel ist eine Anweisung für sich. (Wie wir weiter unten sehen werden, ist der Aufruf einer Funktion keine Anweisung.)

Anweisungen geben keine Werte zurück. Daher kannst du keine let-Anweisung einer anderen Variablen zuweisen, wie es der folgende Code versucht; du wirst einen Fehler erhalten:

Dateiname: src/main.rs

fn main() {
    let x = (let y = 6);
}

Wenn du dieses Programm ausführst, wirst du in etwa folgenden Fehler erhalten:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  |

warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted

Die Anweisung let y = 6 gibt keinen Wert zurück, also gibt es für x nichts, woran x gebunden werden kann. Dies unterscheidet sich von dem, was in anderen Sprachen wie C und Ruby geschieht, wo die Zuweisung den Wert der Zuweisung zurückgibt. In diesen Sprachen kannst du x = y = 6 schreiben und sowohl x als auch y haben den Wert 6; das ist in Rust nicht der Fall.

Ausdrücke werten zu einem Wert aus und machen den größten Teil des restlichen Codes aus, den du in Rust schreiben wirst. Betrachte eine mathematische Operation, z.B. 5 + 6, die ein Ausdruck ist, der zum Wert 11 ausgewertet wird. Ausdrücke können Teil von Anweisungen sein: In Codeblock 3-1 ist die 6 in der Anweisung let y = 6; ein Ausdruck, der den Wert 6 ergibt. Der Aufruf einer Funktion ist ein Ausdruck. Der Aufruf eines Makros ist ein Ausdruck. Ein neuer Gültigkeitsbereichsblock, der mit geschweiften Klammern erstellt wird, ist ein Ausdruck, zum Beispiel:

Dateiname: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("Der Wert von y ist: {y}");
}

Der Ausdruck

{
    let x = 3;
    x + 1
}

ist ein Block, der in diesem Fall zu 4 ausgewertet wird. Dieser Wert wird als Teil der let-Anweisung an y gebunden. Beachte, dass die Zeile x + 1 am Ende kein Semikolon hat, was sich von den meisten Zeilen, die du bisher gesehen hast, unterscheidet. Ausdrücke enthalten keine abschließenden Semikolons. Wenn du ein Semikolon an das Ende eines Ausdrucks anfügst, machst du daraus eine Anweisung, und sie gibt keinen Wert zurück. Behalte dies im Hinterkopf, wenn du als nächstes die Rückgabewerte von Funktionen und Ausdrücken untersuchst.

Funktionen mit Rückgabewerten

Funktionen können Werte an den Code zurückgeben, der sie aufruft. Wir benennen keine Rückgabewerte, aber wir müssen ihren Typ nach einem Pfeil (->) deklarieren. In Rust ist der Rückgabewert der Funktion gleichbedeutend mit dem Wert des letzten Ausdrucks im Block des Funktionsrumpfs. Du kannst frühzeitig von einer Funktion zurückkehren, indem du das Schlüsselwort return verwendest und einen Wert angibst, aber die meisten Funktionen geben den letzten Ausdruck implizit zurück. Hier ist ein Beispiel für eine Funktion, die einen Wert zurückgibt:

Dateiname: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("Der Wert von x ist: {x}");
}

Es gibt keine Funktionsaufrufe, Makros oder gar let-Anweisungen in der five-Funktion – nur die Zahl 5 selbst. Das ist eine vollkommen gültige Funktion in Rust. Beachte, dass der Rückgabetyp der Funktion ebenfalls angegeben ist, mit -> i32. Versuche diesen Code auszuführen; die Ausgabe sollte wie folgt aussehen:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
Der Wert von x ist: 5

Die 5 in five ist der Rückgabewert der Funktion, weshalb der Rückgabetyp i32 ist. Lass uns dies genauer untersuchen. Es gibt zwei wichtige Teile: Erstens zeigt die Zeile let x = five();, dass wir den Rückgabewert einer Funktion verwenden, um eine Variable zu initialisieren. Da die Funktion five den Wert 5 zurückgibt, ist diese Zeile die gleiche wie die folgende:

#![allow(unused)]
fn main() {
let x = 5;
}

Zweitens hat die Funktion five keine Parameter und definiert den Typ des Rückgabewertes, aber der Funktionsrumpf ist eine einsame 5 ohne Semikolon, weil es ein Ausdruck ist, dessen Wert wir zurückgeben wollen.

Sehen wir uns ein weiteres Beispiel an:

Dateiname: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("Der Wert von x ist: {x}");
}

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

Beim Ausführen dieses Codes wird Der Wert von x ist: 6 ausgegeben. Wenn wir aber ein Semikolon an das Ende der Zeile mit x + 1 setzen und es von einem Ausdruck in eine Anweisung ändern, erhalten wir einen Fehler:

Dateiname: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("Der Wert von x ist: {x}");
}

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

Das Kompilieren dieses Codes führt zum folgenden Fehler:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon to return this value

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

Die Hauptfehlermeldung mismatched types offenbart das Kernproblem dieses Codes. Die Definition der Funktion plus_one besagt, dass sie ein i32 zurückgibt, aber Anweisungen werden nicht zu einem Wert ausgewertet, was durch den Einheitstyp () ausgedrückt wird. Daher wird nichts zurückgegeben, was der Funktionsdefinition widerspricht und zu einem Fehler führt. In dieser Ausgabe gibt Rust eine Meldung aus, die möglicherweise helfen kann, dieses Problem zu beheben: Es wird vorgeschlagen, das Semikolon zu entfernen, was den Fehler beheben würde.