Definieren von Rust-Funktionen in LabEx

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Funktionen. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab wirst du lernen, wie du in Rust Funktionen definierst und aufrufst, indem du das fn-Schlüsselwort und die übliche snake-case-Namenskonvention verwendest.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/shadowing("Variable Shadowing") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") subgraph Lab Skills rust/variable_declarations -.-> lab-100389{{"Definieren von Rust-Funktionen in LabEx"}} rust/shadowing -.-> lab-100389{{"Definieren von Rust-Funktionen in LabEx"}} rust/integer_types -.-> lab-100389{{"Definieren von Rust-Funktionen in LabEx"}} rust/function_syntax -.-> lab-100389{{"Definieren von Rust-Funktionen in LabEx"}} rust/expressions_statements -.-> lab-100389{{"Definieren von Rust-Funktionen in LabEx"}} rust/lifetime_specifiers -.-> lab-100389{{"Definieren von Rust-Funktionen in LabEx"}} end

Funktionen

Funktionen sind in Rust-Code weit verbreitet. Du hast bereits eine der wichtigsten Funktionen in der Sprache gesehen: die main-Funktion, die der Einstiegspunkt vieler Programme ist. Du hast auch das fn-Schlüsselwort gesehen, das dir erlaubt, neue Funktionen zu deklarieren.

Erstelle ein neues Projekt namens functions:

cargo new functions
cd functions

Rust-Code verwendet snake case als die übliche Schreibweise für Funktions- und Variablennamen, bei der alle Buchstaben klein geschrieben sind und Unterstriche Wörter trennen. Hier ist ein Programm, das eine Beispiel-Funktionsdefinition enthält:

Dateiname: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Wir definieren eine Funktion in Rust, indem wir fn eingeben, gefolgt vom Funktionsnamen und einer Klammerung. Die geschweiften Klammern sagen dem Compiler, wo der Funktionskörper beginnt und endet.

Wir können jede Funktion, die wir definiert haben, aufrufen, indem wir ihren Namen gefolgt von einer Klammerung eingeben. Da another_function im Programm definiert ist, kann sie aus der main-Funktion heraus aufgerufen werden. Beachte, dass wir another_function nach der main-Funktion im Quellcode definiert haben; wir hätten es auch vorher definieren können. Rust kümmert sich nicht darum, wo du deine Funktionen definierst, nur darum, dass sie irgendwo in einem Gültigkeitsbereich definiert sind, den der Aufrufer sehen kann.

Lass uns ein neues binäres Projekt namens functions starten, um Funktionen weiter zu erkunden. Plaziere das another_function-Beispiel in src/main.rs und führe es aus. 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`
Hello, world!
Another function.

Die Zeilen werden in der Reihenfolge ausgeführt, in der sie in der main-Funktion erscheinen. Zuerst wird die "Hello, world!"-Nachricht ausgegeben, und dann wird another_function aufgerufen und ihre Nachricht ausgegeben.

Parameter

Wir können Funktionen definieren, die Parameter haben, die spezielle Variablen sind, die Teil der Signatur einer Funktion sind. Wenn eine Funktion Parameter hat, kannst du ihr konkrete Werte für diese Parameter übergeben. Technisch gesehen werden die konkreten Werte als Argumente bezeichnet, aber im alltäglichen Gespräch verwenden die Menschen die Wörter Parameter und Argument im Allgemeinen austauschbar für die Variablen in der Definition einer Funktion oder die konkreten Werte, die beim Aufruf einer Funktion übergeben werden.

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!("The value of x is: {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`
The value of x is: 5

Die Deklaration von another_function hat einen Parameter namens x. Der Typ von x wird als i32 angegeben. Wenn wir 5 an another_function übergeben, setzt die println!-Makro 5 an die Stelle, an der die geschweiften Klammern, die x enthalten, im Formatstring waren.

In Funktionssignaturen muss du den Typ jedes Parameters deklarieren. Dies ist eine bewusste Entscheidung in der Rust-Designphilosophie: Das Erfordern von Typangaben in Funktionsdefinitionen bedeutet, dass der Compiler fast nie benötigt, dass du sie an anderen Stellen im Code verwendest, um herauszufinden, welchen Typ du meinst. Der Compiler kann auch hilfreichere Fehlermeldungen geben, wenn er weiß, welche Typen die Funktion erwartet.

Wenn du mehrere Parameter definierst, trenne die Parameterdeklarationen mit Kommas, wie folgt:

Dateiname: src/main.rs

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

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

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

Lass uns versuchen, diesen Code auszuführen. Ersetze das aktuelle Programm in der src/main.rs-Datei deines functions-Projekts durch das vorherige 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`
The measurement is: 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

Funktionskörper bestehen aus einer Reihe von Anweisungen, die optional mit einem Ausdruck enden. Bislang haben die Funktionen, die wir behandelt haben, keinen abschließenden Ausdruck, aber du hast einen Ausdruck als Teil einer Anweisung gesehen. Da Rust eine ausdrucksbasiertes Sprach ist, ist dies eine wichtige Unterscheidung, die zu verstehen ist. Andere Sprachen haben keine gleichen Unterscheidungen, also schauen wir uns an, was Anweisungen und Ausdrücke sind und wie ihre Unterschiede die Funktionskörper beeinflussen.

  • Anweisungen: sind Anweisungen, die eine Aktion ausführen und keinen Wert zurückgeben.
  • Ausdrücke: evaluieren zu einem Ergebniswert. Schauen wir uns einige Beispiele an.

Wir haben tatsächlich bereits Anweisungen und Ausdrücke verwendet. Das Erstellen einer Variable und das Zuweisen eines Werts mit dem let-Schlüsselwort ist eine Anweisung. In Listing 3-1 ist let y = 6; eine Anweisung.

Dateiname: src/main.rs

fn main() {
    let y = 6;
}

Listing 3-1: Eine main-Funktionsdeklaration mit einer Anweisung

Funktionsdefinitionen sind ebenfalls Anweisungen; das gesamte vorherige Beispiel ist in sich eine Anweisung.

Anweisungen geben keine Werte zurück. Daher kannst du eine let-Anweisung nicht an eine andere Variable zuweisen, wie im folgenden Code versucht wird; du bekommst einen Fehler:

Dateiname: src/main.rs

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

Wenn du dieses Programm ausführst, siehst du einen Fehler wie diesen:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for
more information

Die Anweisung let y = 6 gibt keinen Wert zurück, sodass es nichts gibt, an das x gebunden werden kann. Dies unterscheidet sich von anderen Sprachen wie C und Ruby, in denen die Zuweisung den Wert der Zuweisung zurückgibt. In diesen Sprachen kannst du x = y = 6 schreiben und sowohl x als auch y den Wert 6 zuweisen; das ist in Rust nicht der Fall.

Ausdrücke evaluieren zu einem Wert und machen den größten Teil des restlichen Codes aus, den du in Rust schreiben wirst. Betrachte eine mathematische Operation wie 5 + 6, die ein Ausdruck ist, der den Wert 11 ergibt. Ausdrücke können Teil von Anweisungen sein: In Listing 3-1 ist die 6 in der Anweisung let y = 6; ein Ausdruck, der den Wert 6 ergibt. Ein Funktionsaufruf ist ein Ausdruck. Ein Makroaufruf ist ein Ausdruck. Ein neuer Bereichsblock, der mit geschweiften Klammern erstellt wird, ist ein Ausdruck, z.B.:

Dateiname: src/main.rs

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

    println!("The value of y is: {y}");
}

Der Ausdruck [2] ist ein Block, der in diesem Fall den Wert 4 ergibt. Dieser Wert wird als Teil der let-Anweisung [1] an y gebunden. Beachte die Zeile ohne Semikolon am Ende [3], die anders als die meisten Zeilen ist, die du bisher gesehen hast. Ausdrücke enthalten keine abschließenden Semikolons. Wenn du ein Semikolon am Ende eines Ausdrucks hinzufügst, verwandelst du ihn in eine Anweisung, und er wird dann keinen Wert zurückgeben. Halte dies im Hinterkopf, wenn du im nächsten Schritt die Funktionsrückgabewerte und Ausdrücke erkundest.

Funktionen mit Rückgabewerten

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

Dateiname: src/main.rs

fn five() -> i32 {
    5
}

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

    println!("The value of x is: {x}");
}

In der five-Funktion gibt es keine Funktionsaufrufe, Makros oder sogar let-Anweisungen - nur die Zahl 5 selbst. Das ist in Rust eine vollkommen gültige Funktion. Beachte, dass der Rückgabetyp der Funktion ebenfalls angegeben wird, als -> i32. Versuche, diesen Code auszuführen; die Ausgabe sollte so aussehen:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

Die 5 in five ist der Rückgabewert der Funktion, weshalb der Rückgabetyp i32 ist. Betrachten wir dies im Detail. Es gibt zwei wichtige Punkte: Erstens zeigt die Zeile let x = five();, dass wir den Rückgabewert einer Funktion verwenden, um eine Variable zu initialisieren. Da die Funktion five einen 5 zurückgibt, ist diese Zeile gleich der folgenden:

let x = 5;

Zweitens hat die five-Funktion keine Parameter und definiert den Typ des Rückgabewerts, aber der Funktionskörper ist eine einsame 5 ohne Semikolon, weil es ein Ausdruck ist, dessen Wert wir zurückgeben möchten.

Schauen wir uns ein weiteres Beispiel an:

Dateiname: src/main.rs

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

    println!("The value of x is: {x}");
}

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

Wenn du diesen Code ausführst, wird The value of x is: 6 gedruckt. Aber wenn wir ein Semikolon am Ende der Zeile, die x + 1 enthält, platzieren und sie dadurch von einem Ausdruck zu einer Anweisung umwandeln, erhalten wir einen Fehler:

Dateiname: src/main.rs

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

    println!("The value of x is: {x}");
}

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

Das Kompilieren dieses Codes führt zu einem Fehler, wie folgt:

$ 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

Die Hauptfehlermeldung, mismatched types (ungleiche Typen), offenbart das Kernproblem dieses Codes. Die Definition der Funktion plus_one sagt, dass sie einen i32 zurückgeben wird, aber Anweisungen evaluieren nicht zu einem Wert, was durch () (die 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 Nachricht, die möglicherweise helfen soll, dieses Problem zu beheben: Es schlägt vor, das Semikolon zu entfernen, was den Fehler beheben würde.

Zusammenfassung

Herzlichen Glückwunsch! Du hast das Lab zu Funktionen abgeschlossen. Du kannst in LabEx weitere Labs absolvieren, um deine Fähigkeiten zu verbessern.