Funktionen
Jedes Rust Programm hat mindestens eine Funktion,
die main
Funktion:
fn main() { }
Das ist die simpelste Funktionsdeklaration. Wie wir zuvor schon erwähnt haben,
leitet fn
eine Funktion ein. Darauf folgt der Name und
ein leeres paar Klammern, da diese Funktion keine Argumente hat,
und ein paar geschweifte Klammern, die den Funktionskörper repräsentieren.
Hier ist eine Funktion namens foo
:
# #![allow(unused_variables)] #fn main() { fn foo() { } #}
Ok, wie funktioniert das nun mit Argumenten? Hier eine Funktion, die eine Zahl ausgibt:
# #![allow(unused_variables)] #fn main() { fn print_number(x: i32) { println!("x is: {}", x); } #}
Hier ist ein vollständiges Programm, welches print_number
verwendet:
fn main() { print_number(5); } fn print_number(x: i32) { println!("x is: {}", x); }
Wie du sehen kannst funktionieren Funktionsargumente
ähnlich wie let
Deklarationen:
Man fügt dem Namen einen Typ durch ein Doppelpunkt hinzu.
Hier ist ein vollständiges Programm, welches zwei Zahlen addiert und dann ausgibt:
fn main() { print_sum(5, 6); } fn print_sum(x: i32, y: i32) { println!("sum is: {}", x + y); }
Wie du siehst werden Argumente durch ein Komma getrennt. Das gilt sowohl für den Aufruf als auch für die Deklaration von Funktionen.
Anders als bei let
, musst du die Typen von Funktionsargumenten angeben.
Das hier funktioniert nicht:
# #![allow(unused_variables)] #fn main() { fn print_sum(x, y) { println!("sum is: {}", x + y); } #}
Man bekommt diesen Fehler:
```text
expected one of `!`, `:`, or `@`, found `)`
fn print_number(x, y) {
Das ist eine bewusste Designentscheidung. Obwohl das Herleiten der Typen eines kompletten Programmes möglich ist, wie zum Beispiel in Sprachen wie Haskell, wird dennoch häufig dazu geraten die Typen ausdrücklich zu dokumentieren. Wir stimmen zu, dass ausdrückliche Typvermerke in Funktionssignaturen und Typherleitung innerhalb von Funktionskörpern wundervoller Mittelweg ist.
Wie gibt man einen Wert zurück? Hier ist eine Funktion, die einen Wert inkrementiert.
# #![allow(unused_variables)] #fn main() { fn add_one(x: i32) -> i32 { x + 1 } #}
Rust Funktionen geben genau einen Wert zurück. Diesen gibt man nach einem
"Pfeil" an, welcher aus einem Bindestrich (-
), gefolgt von einem
Größer-Gleich Zeichen (>
) besteht.
Die letzte Zeile der Funktion ist automatisch der Rückgabewert der Funktion.
Du wirst sehen, dass hier das Semikolon fehlt. Wenn wir es hinzufügen:
# #![allow(unused_variables)] #fn main() { fn add_one(x: i32) -> i32 { x + 1; } #}
Würden wir einen Fehler bekommen:
error: not all control paths return a value
fn add_one(x: i32) -> i32 {
x + 1;
}
help: consider removing this semicolon:
x + 1;
^
Dies offenbart zwei interessante Aspekte von Rust: Rust ist eine ausdrucksorientierte Sprache [expression-based language]. Es gibt nur zwei Arten von Anweisungen, alles andere ist ein Ausdruck.
Also worin liegt der Unterschied? Ausdrücke geben einen Wert zurück,
Anweisungen nicht. Deswegen bekommen wir hier eine
‘not all control paths return a value’ Meldung:
Die Anweisung x + 1;
gibt keinen Wert zurück.
Es gibt zwei Arten von Anweisungen in Rust:
Deklarations-Anweisungen
und Ausdrucks-Anweisungen
.
Alles andere ist ein Ausdruck.
Lass uns zuerst über Deklarations-Anweisungen sprechen.
In manchen Sprachen können Variablenbindungen auch als Ausdruck geschrieben werden. Wie z.B. in Ruby:
x = y = 5
In Rust jedoch ist die Variablenbindung mit let
kein Ausdruck.
Das Folgende erzeugt einen Fehler beim Kompilieren:
let x = (let y = 5); // expected identifier, found keyword `let`
Der Compiler sagt uns hier, dass er den Beginn eines Ausdrucks erwartet hat,
denn ein let
kann nur eine Anweisung einleiten, aber keinen Ausdruck.
Beachte, dass eine Zuweisung an eine bereits gebundene Variable (z.B. y = 5
)
trotzdem ein Ausdruck ist, auch wenn dieser nicht besonders nützlich ist.
Anders als in anderen Sprachen, wo der zugewiesene Wert zurückgegeben
werden würde, wird in Rust stattdessen das leere Tupel ()
zurückgegeben.
Der Grund dafür ist, dass der zugewiesene Wert nur einen Besitzer
haben kann und einen anderen Wert zurückzugeben wäre zu überraschend:
# #![allow(unused_variables)] #fn main() { let mut y = 5; let x = (y = 6); // x has the value `()`, not `6` #}
Die zweite Art von Anweisung in Rust ist die Ausdrucks-Anweisung. Ihr Zweck ist es jeden Ausdruck in eine Anweisung zu verwandeln. In praktischer Hinsicht erwartet Rusts Grammatik, dass Anweisungen aufeinander folgen. Das bedeutet, dass man Semikolons nutzt um Ausdrücke voneinander zu trennen. Das bedeutet auch, dass Rust anderen Sprachen, welche auch ein Semikolon am Ende einer Zeile haben, sehr ähnlich sieht und man in Rust fast an jedem Ende einer Zeile ein Semikolon sieht.
Wegen welcher Ausnahme sagen wir "fast"?. Du hast sie bereits gesehen und zwar in diesem Code:
# #![allow(unused_variables)] #fn main() { fn add_one(x: i32) -> i32 { x + 1 } #}
Unsere Funktion gibt an ein i32
zurückzugeben, aber mit einem Semikolon
würden wir stattdessen ()
zurückgeben.
Rust versteht, dass wir das wahrscheinlich nicht wollten und schlägt uns in
der Fehlermeldung, die wir sahen, vor das Semikolon zu entfernen.
Frühzeitige Rückgabe
Was ist mit frühzeitiger Rückgabe [early returns]?
Rust hat dafür ein Schlpsselwort namens return
:
# #![allow(unused_variables)] #fn main() { fn foo(x: i32) -> i32 { return x; // we never run this code! x + 1 } #}
return
in der letzten Zeile einer Funktion zu verwenden funktioniert zwar,
aber wird als schlechter Stil angesehen:
# #![allow(unused_variables)] #fn main() { fn foo(x: i32) -> i32 { return x + 1; } #}
Die vorherige Definition ohne return
sieht vielleicht etwas komisch für dich
aus, falls du noch nicht mit ausdrucksorientierten Sprachen gearbeitet hast,
aber du wirst dich mit der Zeit daran gewöhnen.
Divergierende Funktionde
Rust hat eine spezielle Syntax für sogennannte ‘divergierende Funktionen’ [diverging functions], also Funktionen, die niemals zurückkehren:
# #![allow(unused_variables)] #fn main() { fn diverges() -> ! { panic!("This function never returns!"); } #}
panic!()
ist ein Makro, ähnlich wie println!()
, was wir bereits kennen.
Anders jedoch als println!()
sorgt panic!()
dafür, dass der aktuelle
Thread mit einer Fehlermeldung abstürzt. Weil diese Funktion einen Crash hervorruft, kehrt sie niemals zurück, deswegen hat sie den Typ ‘!
’,
was man als ‘divergiert’ liest.
Wenn du zu einer main Funktion einen diverges()
Aufruf hinzufügst
und das Programm ausführst, dann sieht die Ausgabe in etwa so aus:
thread ‘<main>’ panicked at ‘This function never returns!’, hello.rs:2
Wenn du mehr Informationen haben willst, dann kannst du einen Backtrace durch
Setzen der RUST_BACKTRACE
Umgebungsvariable erhalten:
$ RUST_BACKTRACE=1 ./diverges
thread '<main>' panicked at 'This function never returns!', hello.rs:2
stack backtrace:
1: 0x7f402773a829 - sys::backtrace::write::h0942de78b6c02817K8r
2: 0x7f402773d7fc - panicking::on_panic::h3f23f9d0b5f4c91bu9w
3: 0x7f402773960e - rt::unwind::begin_unwind_inner::h2844b8c5e81e79558Bw
4: 0x7f4027738893 - rt::unwind::begin_unwind::h4375279447423903650
5: 0x7f4027738809 - diverges::h2266b4c4b850236beaa
6: 0x7f40277389e5 - main::h19bb1149c2f00ecfBaa
7: 0x7f402773f514 - rt::unwind::try::try_fn::h13186883479104382231
8: 0x7f402773d1d8 - __rust_try
9: 0x7f402773f201 - rt::lang_start::ha172a3ce74bb453aK5w
10: 0x7f4027738a19 - main
11: 0x7f402694ab44 - __libc_start_main
12: 0x7f40277386c8 - <unknown>
13: 0x0 - <unknown>
RUST_BACKTRACE
funktioniert auch mit Cargos run
Befehl:
$ RUST_BACKTRACE=1 cargo run
Running `target/debug/diverges`
thread '<main>' panicked at 'This function never returns!', hello.rs:2
stack backtrace:
1: 0x7f402773a829 - sys::backtrace::write::h0942de78b6c02817K8r
2: 0x7f402773d7fc - panicking::on_panic::h3f23f9d0b5f4c91bu9w
3: 0x7f402773960e - rt::unwind::begin_unwind_inner::h2844b8c5e81e79558Bw
4: 0x7f4027738893 - rt::unwind::begin_unwind::h4375279447423903650
5: 0x7f4027738809 - diverges::h2266b4c4b850236beaa
6: 0x7f40277389e5 - main::h19bb1149c2f00ecfBaa
7: 0x7f402773f514 - rt::unwind::try::try_fn::h13186883479104382231
8: 0x7f402773d1d8 - __rust_try
9: 0x7f402773f201 - rt::lang_start::ha172a3ce74bb453aK5w
10: 0x7f4027738a19 - main
11: 0x7f402694ab44 - __libc_start_main
12: 0x7f40277386c8 - <unknown>
13: 0x0 - <unknown>
Divergierende Funktionen passen mit jedem Typen zusammen:
# #![allow(unused_variables)] #fn main() { let x: i32 = diverges(); let x: String = diverges(); #}
Funktionszeiger
Wir können auch eine Variablenbindung erzeugen, die auf eine Funktion zeigt:
# #![allow(unused_variables)] #fn main() { let f: fn(i32) -> i32; #}
f
ist eine Variable, die auf eine Funktion zeigt, welche ein i32
als
Argument entgegennimmt und ein i32
zurückgibt. Zum Beispiel:
# #![allow(unused_variables)] #fn main() { fn plus_one(i: i32) -> i32 { i + 1 } // without type inference let f: fn(i32) -> i32 = plus_one; // with type inference let f = plus_one; #}
Wir können dann f
benutzen um die Funktion aufzurufen:
# #![allow(unused_variables)] #fn main() { let six = f(5); #}