Initial astro commit
This commit is contained in:
52
src/content/rust/00 - Hello World.mdx
Normal file
52
src/content/rust/00 - Hello World.mdx
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Hello world!
|
||||
published: 2022-08-10T17:04:53+02:00
|
||||
sorting: 0
|
||||
slug: hello-world
|
||||
---
|
||||
|
||||
# Hello world!
|
||||
|
||||
_[Link zum Buch](https://doc.rust-lang.org/book/ch01-02-hello-world.html)_
|
||||
|
||||
## How to `println!`
|
||||
|
||||
Hello world ist relativ einfach. `println!` ist ein Makro (eine
|
||||
spezielle Art Funktion?), die einfach auf stdout printed.
|
||||
|
||||
|
||||
```rust
|
||||
println!("Hello world!");
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Hello world!
|
||||
```
|
||||
|
||||
## Komplettes Programm
|
||||
|
||||
Rust hat ähnlich wie C eine `main`-Funktion, die zum Start ausgeführt
|
||||
wird.
|
||||
Ein komplettes Programm zum Kompilieren hätte also den folgenden Inhalt:
|
||||
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello world!");
|
||||
}
|
||||
```
|
||||
|
||||
Kompiliert und ausgeführt wird es dann über folgende Befehle:
|
||||
|
||||
```bash
|
||||
$ rustc main.rs
|
||||
$ ./main
|
||||
Hello world!
|
||||
```
|
||||
|
||||
## Weitere Details
|
||||
|
||||
* `fn` -> Funktionsdeklaration
|
||||
* 4 Leerzeichen zum Einrücken, kein Tab
|
||||
* `;` am Ende der Zeile
|
||||
74
src/content/rust/01 - Cargo.mdx
Normal file
74
src/content/rust/01 - Cargo.mdx
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Cargo
|
||||
published: 2022-10-18T17:56:26+02:00
|
||||
sorting: 1
|
||||
slug: cargo
|
||||
---
|
||||
|
||||
# Cargo
|
||||
|
||||
_[Link zum Buch](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html)_
|
||||
|
||||
## Was ist Cargo?
|
||||
|
||||
Cargo ist Rusts package manager.<br/>
|
||||
Um ein neues Cargo-Projekt zu erstellen, braucht es das folgende
|
||||
Command:
|
||||
|
||||
```bash
|
||||
$ cargo new projektname --bin
|
||||
```
|
||||
|
||||
`--bin` sagt, dass wir ein neues Binary erstellen und keine
|
||||
Bibliothek.<br/>
|
||||
Es wird auch gleich `main.rs`, ein `.git`-Ordner (inkl. `.gitignore`)
|
||||
und `Cargo.toml` erstellt.
|
||||
|
||||
## Angelegte Dateien
|
||||
|
||||
### Cargo.toml
|
||||
|
||||
Unangetastet sieht die Datei so aus:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "projektname"
|
||||
version = "0.1.0"
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
|
||||
[dependencies]
|
||||
```
|
||||
|
||||
Hier können also Meta-Infos wie Name und Dependencies gespeichert
|
||||
werden.
|
||||
So wie eine `package.json` in JavaScript.
|
||||
|
||||
### main.rs
|
||||
|
||||
Die Main-Datei ist mit ``Hello World'' gefüllt.
|
||||
|
||||
## Commands
|
||||
|
||||
### cargo build
|
||||
|
||||
```bash
|
||||
$ cargo build
|
||||
$ ./target/debug/projektname
|
||||
```
|
||||
|
||||
Standardmäßig wird ein Debug-Build erzeugt. `cargo build --release`
|
||||
erzeugt einen Release-Build.
|
||||
|
||||
### cargo run
|
||||
|
||||
Macht einen build und führt die Datei dann aus.
|
||||
|
||||
### cargo check
|
||||
|
||||
Checkt alles einmal durch.
|
||||
|
||||
### cargo update
|
||||
|
||||
Updatet alle Dependencies. Allerdings nur auf die letzte Subversion der
|
||||
angegebenen Version. Will man eine neue Version, muss man das manuell
|
||||
angeben.
|
||||
313
src/content/rust/_02 - Higher-Lower-Spiel.adoc
Normal file
313
src/content/rust/_02 - Higher-Lower-Spiel.adoc
Normal file
@@ -0,0 +1,313 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-10-18T17:56:26+02:00
|
||||
|
||||
= Erstes Spiel
|
||||
|
||||
_https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html[Link zum Buch]_ | _Diese Seite ist aus einem https://jupyter.org/[Jupyter Notebook] exportiert_.
|
||||
|
||||
== Projekt erstellen
|
||||
|
||||
Das Projekt wird wie in Notebook 01 beschrieben erstellt.
|
||||
|
||||
== Einen Input aufnehmen
|
||||
|
||||
|
||||
~*In[2]:*~
|
||||
[source, rust]
|
||||
----
|
||||
:dep evcxr_input
|
||||
// Das ^ ist für Jupyter
|
||||
// Das v würde man wirklich benutzen
|
||||
// use std::io;
|
||||
|
||||
println!("Guess the number!");
|
||||
|
||||
println!("Please input your guess.");
|
||||
|
||||
let mut guess = evcxr_input::get_string("Number? ");
|
||||
// Das ^ ist für Jupyter
|
||||
// Das v würde man wirklich benutzen
|
||||
//let mut guess = String::new();
|
||||
|
||||
//io::stdin().read_line(&mut guess)
|
||||
// .expect("Failed to read line");
|
||||
|
||||
println!("You guessed: {}", guess);
|
||||
----
|
||||
|
||||
|
||||
~*Out[2]:*~
|
||||
----
|
||||
Guess the number!
|
||||
Please input your guess.
|
||||
Number? 42
|
||||
You guessed: 42
|
||||
----
|
||||
|
||||
== Was haben wir gemacht?
|
||||
|
||||
* `use std::io;` bindet die Standard-IO Bibliothek ein
|
||||
* `let mut guess` legt eine Variable `guess` an
|
||||
** `mut` bedeutet, dass sie ``mutable'' also veränderbar ist
|
||||
* `String::new()` erstellt eine neue Instanz der `String`-Klasse
|
||||
* `io::stdin()` legt ein `Stdin`-Objekt an - ein Handler für die
|
||||
CLI-Eingabe
|
||||
** ohne die ``use'' Anweisung oben, müsste es `std::io::stdin()` sein
|
||||
* `.read_line(&mut guess)` ließt eine Zeile und speichert sie in guess
|
||||
** `&` erstellt dabei eine Referenz (wie in C)
|
||||
** Referenzen sind standardmäßig immutable - deshalb `&mut`
|
||||
** `read_line()` gibt ein `Result`-Objekt zurück, dieser kann `Ok` oder
|
||||
`Err` enthalten
|
||||
* `.expect("Fehlermeldung")` entpackt das `Result`-Objekt
|
||||
** Theoretisch ist das unnötig, sonst gibt es aber eine Warnung
|
||||
** Sollte ein `Err` im Result sein, wird durch `expect()` eine Exception
|
||||
auftreten
|
||||
* `println!("Eingabe: {}", guess)` ist ein formatiertes print
|
||||
|
||||
== Eine random Zahl erstellen
|
||||
|
||||
Für eine random Zahl brauchen wir die erste Dependency. +
|
||||
Also `Cargo.toml` bearbeiten:
|
||||
|
||||
[source, toml]
|
||||
----
|
||||
[dependencies]
|
||||
rand = "0.3.14"
|
||||
----
|
||||
|
||||
(In Jupyter müssen wir das anders lösen.)
|
||||
|
||||
Dependencies findet man auch auf https://crates.io[crates.io].
|
||||
|
||||
Die crate `rand` kann jetzt im Code verwendet werden.
|
||||
|
||||
|
||||
~*In[3]:*~
|
||||
[source, rust]
|
||||
----
|
||||
:dep rand = "0.3.15"
|
||||
// Das ^ ist von Jupyter
|
||||
|
||||
extern crate rand;
|
||||
use rand::Rng;
|
||||
|
||||
let secret_number: u32 = rand::thread_rng().gen_range(1, 101);
|
||||
|
||||
println!("{}", secret_number);
|
||||
----
|
||||
|
||||
|
||||
~*Out[3]:*~
|
||||
----
|
||||
37
|
||||
----
|
||||
|
||||
== Höher oder tiefer?
|
||||
|
||||
Vergleichen wir doch einfach mal… +
|
||||
Ein Fehler?
|
||||
|
||||
|
||||
~*In[4]:*~
|
||||
[source.notCompiling, rust]
|
||||
----
|
||||
use std::cmp::Ordering;
|
||||
|
||||
match guess.cmp(&secret_number) {
|
||||
Ordering::Less => println!("Too small!"),
|
||||
Ordering::Greater => println!("Too big!"),
|
||||
Ordering::Equal => println!("You win!"),
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
~*Out[4]:*~
|
||||
----
|
||||
|
||||
match guess.cmp(&secret_number) {
|
||||
|
||||
^^^^^^^^^^^^^^ expected struct `String`, found `u32`
|
||||
|
||||
mismatched types
|
||||
|
||||
----
|
||||
|
||||
Unser `guess` ist ja ein `String`! Den kann man nicht einfach mit einem
|
||||
`int` vergleichen (anscheinend). +
|
||||
Wir müssen unser guess also umwandeln:
|
||||
|
||||
|
||||
~*In[5]:*~
|
||||
[source, rust]
|
||||
----
|
||||
let guess: u32 = guess.trim().parse().expect("Please type a number!");
|
||||
----
|
||||
|
||||
`.strip()` entfernt Whitespace von beiden Seiten und `parse()` macht
|
||||
eine Zahl draus.
|
||||
|
||||
`guess` als Variable ist schon vorhanden? Kein Problem! Rust erlaubt
|
||||
``Shadowing'', damit man nicht mehrere Variablen unterschiedlicher
|
||||
Datentypen für den selben Wert anlegen muss.
|
||||
|
||||
Jetzt sollte das Vergleichen auch klappen!
|
||||
|
||||
|
||||
~*In[6]:*~
|
||||
[source, rust]
|
||||
----
|
||||
use std::cmp::Ordering;
|
||||
|
||||
match guess.cmp(&secret_number) {
|
||||
Ordering::Less => println!("Too small!"),
|
||||
Ordering::Greater => println!("Too big!"),
|
||||
Ordering::Equal => println!("You win!"),
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
~*Out[6]:*~
|
||||
----
|
||||
Too big!
|
||||
()
|
||||
----
|
||||
|
||||
== Nicht nur ein Versuch
|
||||
|
||||
Damit wir mehrmals raten können, brauchen wir eine Schleife.
|
||||
|
||||
|
||||
~*In[7]:*~
|
||||
[source, rust]
|
||||
----
|
||||
let secret_number: u32 = rand::thread_rng().gen_range(1, 101);
|
||||
loop {
|
||||
let mut guess = evcxr_input::get_string("Number? ");
|
||||
let guess: u32 = guess.trim().parse().expect("Please type a number!");
|
||||
|
||||
match guess.cmp(&secret_number) {
|
||||
Ordering::Less => println!("Too small!"),
|
||||
Ordering::Greater => println!("Too big!"),
|
||||
Ordering::Equal => println!("You win!"),
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
~*Out[7]:*~
|
||||
----
|
||||
Number? 100
|
||||
Too big!
|
||||
Number? 50
|
||||
Too big!
|
||||
Number? 25
|
||||
Too small!
|
||||
Number? 30
|
||||
Too small!
|
||||
Number? 40
|
||||
Too small!
|
||||
Number? 42
|
||||
Too small!
|
||||
Number? 45
|
||||
You win!
|
||||
Number? 45
|
||||
You win!
|
||||
Number? 100
|
||||
Too big!
|
||||
Number? 45
|
||||
You win!
|
||||
Number?
|
||||
...
|
||||
----
|
||||
|
||||
Funktioniert, aber selbst nach dem Erraten passiert nichts und wir
|
||||
sollen weiter raten. +
|
||||
Offensichtlich müssen wir die Schleife dann abbrechen.
|
||||
|
||||
|
||||
~*In[8]:*~
|
||||
[source, rust]
|
||||
----
|
||||
let secret_number: u32 = rand::thread_rng().gen_range(1, 101);
|
||||
loop {
|
||||
let mut guess = evcxr_input::get_string("Number? ");
|
||||
let guess: u32 = guess.trim().parse().expect("Please type a number!");
|
||||
|
||||
match guess.cmp(&secret_number) {
|
||||
Ordering::Less => println!("Too small!"),
|
||||
Ordering::Greater => println!("Too big!"),
|
||||
Ordering::Equal => {
|
||||
println!("You win!");
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
~*Out[8]:*~
|
||||
----
|
||||
Number? 100
|
||||
Too big!
|
||||
Number? 50
|
||||
Too big!
|
||||
Number? 25
|
||||
Too small!
|
||||
Number? 42
|
||||
Too big!
|
||||
Number? 39
|
||||
Too big!
|
||||
Number? 37
|
||||
Too big!
|
||||
Number? 36
|
||||
Too big!
|
||||
Number? 33
|
||||
Too big!
|
||||
Number? 30
|
||||
Too big!
|
||||
Number? 29
|
||||
You win!
|
||||
()
|
||||
----
|
||||
|
||||
== Error handling
|
||||
|
||||
Derzeit stirbt das Programm einfach mit einem Fehler, wenn man keine
|
||||
Zahl eingibt. Das können wir auch relativ einfach fixen:
|
||||
|
||||
|
||||
~*In[9]:*~
|
||||
[source, rust]
|
||||
----
|
||||
loop {
|
||||
let mut guess = evcxr_input::get_string("Number? ");
|
||||
let guess: u32 = match guess.trim().parse() {
|
||||
Ok(num) => num,
|
||||
Err(_) => continue,
|
||||
};
|
||||
// Wenn wir hier her kommen, haben wir eine gültige Zahl und beenden einfach.
|
||||
break;
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
~*Out[9]:*~
|
||||
----
|
||||
Number? a
|
||||
Number? b
|
||||
Number? 🦀
|
||||
Number? 5
|
||||
()
|
||||
----
|
||||
|
||||
Statt einem `expect()` haben wir nun eine `match`-Expression. Die Syntax
|
||||
ist relativ einfach zu verstehen. Man kann auch mehrere `Ok(value)`
|
||||
nutzen, wobei dann das richtige aufgerufen wird. `Err(_)` nutzt den
|
||||
Unterstrich, um alle Fehler zu catchen, nicht nur einen speziellen.
|
||||
|
||||
Das `num` nach dem Pfeil ist ein implizites Return. Wenn eine Variable
|
||||
am Ende eines Blocks steht, wird sie zurückgegeben.
|
||||
|
||||
== Fertig
|
||||
|
||||
Wir haben nun alle Elemente für das ``Higher-Lower-Game''.
|
||||
291
src/content/rust/_03 - Concepts.adoc
Normal file
291
src/content/rust/_03 - Concepts.adoc
Normal file
@@ -0,0 +1,291 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-10-18T17:56:26+02:00
|
||||
|
||||
= Konzepte
|
||||
|
||||
https://doc.rust-lang.org/book/ch03-00-common-programming-concepts.html[Link zum Buch]
|
||||
|
||||
|
||||
== Variablen
|
||||
=== Mutability
|
||||
|
||||
Standardmäßig sind Variablen nicht mutable, also nicht veränderbar.
|
||||
In anderen Sprachen ist das häufig `const` - in Rust gibt es aber auch `const`!
|
||||
|
||||
Das Folgende funktioniert also nicht:
|
||||
|
||||
[source.notCompiling, Rust]
|
||||
----
|
||||
fn main() {
|
||||
let x = "Hello world!";
|
||||
// Das folgende funktioniert nicht, weil x nicht mutable ist!
|
||||
x = "Hello Rust!";
|
||||
}
|
||||
----
|
||||
|
||||
Damit Variablen mutable sind, muss `mut` genutzt werden:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn main() {
|
||||
let mut x = "Hello world!";
|
||||
// Hier funktioniert es.
|
||||
x = "Hello Rust!";
|
||||
}
|
||||
----
|
||||
|
||||
=== Constants
|
||||
|
||||
Neben unveränderlichen Variablen gibt es auch noch Konstanten.
|
||||
Diese sind sehr ähnlich zu ersteren, haben aber zwei relevante Unterschiede:
|
||||
|
||||
- Der Typ *muss* angegeben werden. Type inference funktioniert hier nicht.
|
||||
- Konstanten können nur auf zu Compilezeit konstante Ausdrücke gesetzt werden, keine zu Runtime veränderlichen.
|
||||
|
||||
Die Konvention für Konstanten ist snake case all caps.
|
||||
|
||||
Ein Beispiel dafür ist folgendes:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
const MINUTES_IN_A_DAY: u32 = 24 * 60;
|
||||
----
|
||||
|
||||
=== Shadowing
|
||||
|
||||
Shadowing wurde beim Higher-Lower-Game schon einmal erwähnt.
|
||||
Anfangs habe ich es falsch verstanden: Ich dachte Shadowing wäre, dass eine Variable unter dem selben Namen in unterschiedlichen Datentypen vorhanden wäre.
|
||||
|
||||
Allerdings ist es mehr ein "Reuse" eines alten Namens.
|
||||
Ein Beispiel:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn main() {
|
||||
let x = 5;
|
||||
let x = x + 5;
|
||||
|
||||
println!("{}", x);
|
||||
}
|
||||
----
|
||||
|
||||
Die Ausgabe des Programms ist dabei der letztere Wert, hier also 10.
|
||||
Es ist also mehr eine neue Variable unter dem selben Namen wie die alte.
|
||||
Sogar der Datentyp kann sich dabei ändern, man muss sich also nicht ständig neue Namen für Variablen ausdenken, nur weil man sie casted (Juchuu!).
|
||||
|
||||
Da Variablen immer Block-Scope-basiert (?) sind, kann dies natürlich auch in einem eingebetteten Block genutzt werden.
|
||||
|
||||
Der Unterschied zu mutable Variablen ist ganz einfach: neben einigen Unterschieden unter der Haube (oder?), haben mutable Variablen einen festen Datentyp, der nicht einfach geändert werden kann.
|
||||
|
||||
== Datentypen
|
||||
|
||||
=== Data Inference
|
||||
Jede Variable hat einen festen Datentyp.
|
||||
Der Compiler kann häufig selber herausfinden, was für einer das ist, das ist die "Type Inference".
|
||||
Wenn das nicht geht, muss manuell ein Typ festgelegt werden.
|
||||
|
||||
Ein Beispiel:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let guess: u32 = "42".parse().expect("Not a number!");
|
||||
----
|
||||
|
||||
`"42"` ist offensichtlich ein String.
|
||||
`parse()` kann verschiedene Ergebnisse-Datentypen erzeugen.
|
||||
Das Ergebnis kann also verschiedene Typen haben, wir wollen ja aber wissen, was `guess` ist.
|
||||
Hier muss also `guess: u32` angegeben werden, sonst gibt es einen Fehler vom Compiler.
|
||||
|
||||
=== Scalar Types
|
||||
Skalar heißt: ein einziges Value.
|
||||
Also eine Zahl (integer/float), Boolean oder ein einzelner Character.
|
||||
|
||||
==== Integer
|
||||
Es signed und unsigned Integer und verschiedener Länge - 8, 16, 32, 64 und 128 Bit und "size".
|
||||
"size" ist dabei architektur-abhängig, also zumeist 32 oder 64 Bit.
|
||||
|
||||
- signed sind im Zweierkomplement
|
||||
- man kann den Datentyp direkt an eine Zahl anhängen (`5u32`)
|
||||
- man kann in Dezimalschreibweise beliebig `_` einfügen für Lesbarkeit (z.B. `1_000`)
|
||||
- außerdem schreibbar in hex (`0x...`), oct (`0o...`), bin (`0b...`) oder als Byte (`b'A'`)
|
||||
- Division zweier Integer erzeugt einen Integer (abgerundet)
|
||||
|
||||
Overflows sind interessant: Wenn zu Debug compiled wird, gibt es ein panic und das Programm beendet mit einem Fehler (nicht auffangbar).
|
||||
In Release ist es dann die "normale" Variante mit einem Wrap-around. +
|
||||
Interessant ist, dass es zusätzliche Methoden für alles gibt (nicht nur `add`):
|
||||
|
||||
- `wrapping_add` ersetzt das normale Addieren und wrapt
|
||||
- `checked_add` wirft einen abfangbaren Fehler bei Overflow
|
||||
- `overflowing_add` gibt einen Boolean, ob ein Overflow auftritt
|
||||
- `saturating_add` bleibt beim Maximum oder Minimum des verfügbaren Bereiches
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let number: u8 = 254;
|
||||
println!("{}", number.wrapping_add(2));
|
||||
----
|
||||
|
||||
Die Ausgabe des Programms ist 0.
|
||||
|
||||
==== Floats
|
||||
Sind normale IEEE-754 floats mit 32 oder 64 Bit.
|
||||
|
||||
==== Boolean
|
||||
Auch nichts besonders, `true` oder `false` halt.
|
||||
|
||||
==== Chars
|
||||
Sind besonders.
|
||||
Einzelne Character in Rust sind nicht einfach wie in C ein u8 unter anderem Namen, sondern wirklich ein Zeichen.
|
||||
Jeder Unicode-Character ist ein Char, also auch `'🐧'`.
|
||||
Chars werden mit single-quotes geschrieben (Strings mit doppelten quotes).
|
||||
|
||||
Allerdings scheint es noch ein wenig komplizierter zu sein, das kommt aber erst später.
|
||||
|
||||
=== Compound Types
|
||||
Gruppierung von mehreren Werten in einem Typ.
|
||||
|
||||
==== Tupel
|
||||
Tupel sind weird.
|
||||
Sie haben eine feste Länge (wie C-Arrays), können aber verschiedene Datentypen beinhalten, also wie in Python.
|
||||
Sie sind aber schreibbar, wenn `mut` zur Initialisierung genutzt wird, also nicht wie in Python.
|
||||
|
||||
Ein paar Beispiele als Code:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let x: (f32, char, u8) = (1.0, '🐧', 3);
|
||||
//_x.0 = 2.0; // geht nicht, da x nicht mut ist.
|
||||
|
||||
let mut x: (f32, char, u8) = x;
|
||||
|
||||
println!("{}", x.0); // x.0 == x[0] -> 1.0
|
||||
|
||||
// Dekonstruktur. Wie in JS wird einfach zugewiesen.
|
||||
let (_a, b, _c) = x; // a = x.0 = 1.0, b = x.1 = 🐧, c = x.2 = 3
|
||||
println!("{}", b); // b is 🐧
|
||||
|
||||
x.2 = 4; // x.2 ist schreibbar, wenn x mut ist.
|
||||
println!("{}", x.2);
|
||||
|
||||
//x.2 = 1.0; // Das geht nicht, da x.2 ein u8 ist.
|
||||
----
|
||||
|
||||
Falls eine Funktion in Rust nichts zurückgibt, gibt sie in leeres Tupel `()`, auch `unit type` genannt, zurück.
|
||||
|
||||
==== Arrays
|
||||
Arrays sind wie C-Arrays, haben also eine feste Länge und nur einen Datentyp.
|
||||
Für "Arrays" mit veränderbarer Länge gibt es Vektoren.
|
||||
|
||||
Wieder etwas Code:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let x: [i32; 5] = [1, 2, 3, 4, 5];
|
||||
// ^ so sieht der Datentyp aus
|
||||
|
||||
println!("{}", x[0]); // 1, so wie immer
|
||||
|
||||
let mut x = [15; 3]; // -> [15, 15, 15]
|
||||
x[0] = 16; // x = [16, 15, 15]
|
||||
----
|
||||
|
||||
Im Gegensatz zu C-Arrays wird allerdings vor dem Zugriff auf das Array ein Check durchgeführt.
|
||||
Während C also auch außerhalb des Arrays Speicher lesen kann (mindestens theoretisch), kommt es in Rust dann zu einem Compilerfehler oder einer Runtime-Panic.
|
||||
|
||||
== Funktionen
|
||||
|
||||
Sind wie normale Funktionen in C auch. Keyword ist `fn`.
|
||||
|
||||
Beispiel:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn calculate_sum(a: i32, b: i32) -> i64 {
|
||||
// Statements können natürlich normal genutzt werden
|
||||
let c: i64 = a + b
|
||||
|
||||
// Wenn das letzte Statement kein ";" am Ende hat, ist es die Rückgabe
|
||||
// Quasi "return c;"
|
||||
// "let ...." returnt aber nichts
|
||||
// Könnte aber auch einfach nur "a + b" sein.
|
||||
c
|
||||
}
|
||||
----
|
||||
|
||||
== Kommentare
|
||||
|
||||
Schon häufiger in den Beispielen - einfach `//`.
|
||||
Es gibt auch noch spezielle Docstrings, aber das kommt später.
|
||||
|
||||
== Kontrollfluss
|
||||
=== `if`
|
||||
|
||||
- ohne runde Klammern um die Bedingung
|
||||
- _immer_ geschweifte Klammern, zumindest kein Beispiel ohne
|
||||
- Geht auch als short-if bei `let x = if condition { 5 } else { 6 }`
|
||||
- Bedingung *muss* ein bool sein!
|
||||
|
||||
=== `loop`
|
||||
|
||||
- Basically ein `while (true)`
|
||||
- `break` und `continue`
|
||||
- Können labels haben. Dann kann `break 'label` genutzt werden
|
||||
|
||||
Beispiel für labels:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn main() {
|
||||
'outer: loop {
|
||||
let mut a = 1;
|
||||
loop {
|
||||
a += 1;
|
||||
if a == 10 {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
==== Ergebnis aus der Loop
|
||||
|
||||
`break` mit Wert ist Rückgabe.
|
||||
Einfaches Beispiel:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn main() {
|
||||
let mut counter = 0;
|
||||
|
||||
let result = loop {
|
||||
counter += 1;
|
||||
|
||||
if counter == 10 {
|
||||
break counter * 2;
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}", result); // 20
|
||||
}
|
||||
----
|
||||
|
||||
=== `while`
|
||||
|
||||
- nutzt auch keine runden Klammern
|
||||
- sonst normal
|
||||
|
||||
=== `for`
|
||||
|
||||
Looped durch eine Collection (wie in Python).
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn main() {
|
||||
let a = [10, 20, 30, 40, 50];
|
||||
|
||||
for element in a {
|
||||
println!("{}", element);
|
||||
}
|
||||
}
|
||||
----
|
||||
151
src/content/rust/_04 - Ownership.adoc
Normal file
151
src/content/rust/_04 - Ownership.adoc
Normal file
@@ -0,0 +1,151 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-10-18T17:56:26+02:00
|
||||
|
||||
= Ownership
|
||||
|
||||
https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html[Link zum Buch]
|
||||
|
||||
== Was ist das?
|
||||
|
||||
Jeder Wert hat eine Variable, die ihn "besitzt".
|
||||
Jeder Wert kann zu einem Zeitpunkt nur von _einer_ Variable besessen werden.
|
||||
Sollte die Variable aus dem Scope verschwinden, wird der Wert ungültig und aus dem Speicher entfernt.
|
||||
|
||||
== Warum?
|
||||
|
||||
Wenn ein Wert eine feste Länge hat, kann man sie ganz einfach auf den Stack packen.
|
||||
Falls die Länge aber variabel ist, muss zu Laufzeit Speicher allokiert werden.
|
||||
In C ist das dann der Aufruf `malloc`, der einen Pointer zu dem entsprechenden Bereich gibt.
|
||||
Später muss dann `free` aufgerufen werden, um den Speicher wieder freizugeben, weil C keinen Garbage Collector hat, der sich alleine darum kümmert.
|
||||
|
||||
Es ist wenig verwunderlich, dass beim manuellen Aufruf von `malloc` und `free` allerhand schiefgehen kann.
|
||||
Entweder kann Speicher zu früh (eventuell falsche Werte) oder zu spät (höherer Speicherverbrauch) freigegeben werden, oder auch zum Beispiel doppelt.
|
||||
|
||||
Rust nutzt deshalb (wenn man das nicht aktiv anders macht) einen anderen Ansatz, bei dem der Compiler selber `drop` (was in etwa `free` entspricht) einfügt, wenn eine Variable aus dem Scope verschwindet.
|
||||
|
||||
== Was das für den Code bedeutet
|
||||
|
||||
=== String Datentyp
|
||||
|
||||
Fangen wir mal mit einem Datentypen an, den das betrifft.
|
||||
|
||||
Neben String literals gibt es auch noch einen anderen String-Typen, Rust scheint da sich ein wenig an OOP zu orientieren.
|
||||
Im Higher-Lower-Game wurde der auch schon benutzt, und User-Input mit `String::from(x)` in eine Variable gelegt.
|
||||
Dieser String-Typ hat den Vorteil, dass er eine dynamische Länge hat und damit verändert werden kann.
|
||||
|
||||
Ein Beispiel:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let mut x = String::from("Hello"); // Legt "dynamischen" String an
|
||||
x.push_str(" world!"); // Konkatiniert an den String
|
||||
|
||||
println!("{}", x); // "Hello world!"
|
||||
----
|
||||
|
||||
Das geht mit den normalen String-Literalen (`let mut x = "Hello";`) nicht, da diese eine immer eine feste Länge haben.
|
||||
Theoretisch kann `x` natürlich dann überschrieben werden, mit einem String anderer Länge, aber anscheinend wird das von Rust überdeckt und wahrscheinlich ähnlich wie Shadowing gehandhabt.
|
||||
|
||||
=== Move
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let x = 5; // Int -> feste Größe und auf Stack
|
||||
let y = x;
|
||||
|
||||
let s1 = String::from("Hello world"); // Dynamischer String auf Heap
|
||||
let s2 = s1;
|
||||
----
|
||||
|
||||
Hier trifft ähnliches zu, wie zum Beispiel in Python: primitive Datentypen, wie `int` oder `float`, werden einfach kopiert, wenn sie einer anderen Variable zugewiesen werden.
|
||||
Bei Objekten auf dem Heap dagegen, wird auch kopiert, allerdings nur was wirklich in `s1` steht: die Referenz auf den Speicher (also ein Pointer), die Länge und andere "Metadaten".
|
||||
|
||||
In Sprachen mit Garbage Collector also Java oder Python haben `s1` und `s2` jetzt zu jeder Zeit den gleichen Wert.
|
||||
Sollte eines verändert werden, wird das zweite auch verändert.
|
||||
Sehr tückisch manchmal.
|
||||
|
||||
Rust löst es anders: Damit nicht zum Beispiel ein doppeltes `free` auftritt, wird hier `s1` invalidiert, nachdem es in eine andere Variable gegeben wurde.
|
||||
Wie oben beschrieben: Der Wert von `s1` kann nur einen Besitzer haben und der wird mit der letzten Zeile gewechselt.
|
||||
Sollte man nach dem Snipped ein print nutzen, gäbe es einen Compile-Fehler.
|
||||
|
||||
Natürlich gibt es auch Wege für ein "deep copy", allerdings ist das nie der Standard.
|
||||
Die Methode dafür (muss natürlich implementiert sein) heißt `clone`.
|
||||
|
||||
Wir könnten also auch schreiben `let s2 = s1.clone()` und beide Variablen wären unabhängig voneinander und gültig.
|
||||
Das kann aber sehr teuer für die Laufzeit sein!
|
||||
|
||||
=== Copy und Drop Annotation
|
||||
|
||||
Im Buch wird jetzt noch kurz angeschnitten, dass diese primitiven Datentypen kopiert werden, weil sie das `Copy` "trait" implementiert hätten.
|
||||
An dem Punkt habe ich noch keine Ahnung, was das ist, aber ich denke es wird so ähnlich sein, wie Java Interfaces?
|
||||
|
||||
Wenn ein Datentyp den `Copy` trait hat, wird es auf jeden Fall einfach kopiert, statt gemoved.
|
||||
|
||||
Es gibt auch ein `Drop` trait, mit dem noch irgendwas ausgeführt werden kann, wenn ein Wert dieses Types gedropped wird. Dieser trait ist exklusiv zu `Copy`.
|
||||
|
||||
== In Funktionen
|
||||
|
||||
Sollte eine Funktion eine Variable übergeben bekommen, wird auch das Ownership der Variable dahin übergeben.
|
||||
Nach Ausführen der Funktion ist die Variable ungültig.
|
||||
Der Wert wird aber möglicherweise wieder zurückgegeben.
|
||||
Das gilt natürlich nicht für die `Copy`-Datentypen.
|
||||
|
||||
Wie vorher schon erfahren, kann man auch Referenzen und mutable Referenzen übergeben, wodurch die Variable nur "geborgt" wird.
|
||||
|
||||
In C/C++ gibt es ja beim Aufruf von zum Beispiel Funktionen eines Structs oder Objekt, den Pfeil (`x->fun()`) der quasi auch nur ein hübsches `(*x).fun()` ist.
|
||||
|
||||
In Rust sag ich der Funktion, dass ein Argument eine Referenz auf einen Datentypen ist, und ich kann mit der Variable arbeiten, als wäre sie da.
|
||||
Wenn ich den Wert der Referenz haben will, muss ich sie natürlich immer noch dereferenzieren.
|
||||
|
||||
Solange die Referenz nicht mutable ist, können davon unendlich viele existieren.
|
||||
|
||||
Mutable References werden noch wieder kritisch behandelt - es kann zu einem Zeitpunkt immer nur eine mutable Referenz geben (ähnlich Ownership also).
|
||||
Noch krasser: Eine mutable Referenz kann nicht erstellt werden, solange eine immutable existiert.
|
||||
"Existieren" bedeutet hier natürlich: Wenn die immutable Referenz nach Erstellen der mutable Referenz noch einmal genutzt wird.
|
||||
Sonst kümmert sich der Compiler drum.
|
||||
|
||||
Das heißt natürlich auch, dass alle immutable Referenzen invalid werden, sobald eine mutable Referenz erstellt wird.
|
||||
|
||||
Damit werden (unter anderem) Race Conditions schon beim Compilen verhindert.
|
||||
|
||||
=== Dangling references
|
||||
|
||||
[source.notCompiling, rust]
|
||||
----
|
||||
fn dangle() -> &String {
|
||||
let s = String::from("hello");
|
||||
&s // Referenz auf s returned
|
||||
} // Hier fliegt s aus dem Scope
|
||||
----
|
||||
|
||||
Hier ist eine Funktion gebaut, die nur eine Referenz zurückgibt.
|
||||
Allerdings wird `s` ja (da nach Funktion out of scope) nach der Funktion gedropped.
|
||||
Der Compiler gibt uns dafür auch einen Fehler.
|
||||
|
||||
Das Tutorial sagt an diesem Punkt, dass man am besten keine Referenzen zurückgibt, die Fehlermeldung redet aber auch noch von "lifetimes" und dass `&'static String` ein möglicher Rückgabetyp wäre.
|
||||
Das kommt wohl aber erst später...
|
||||
|
||||
== Der Slice-Datentyp
|
||||
|
||||
Wenn wir auf Arrays arbeiten, wäre es ja cool, an verschiedenen Stellen gleichzeitig zu arbeiten.
|
||||
Nur so kann multithreading etc. funktionieren.
|
||||
|
||||
Dafür hat Rust den Slice-Datentyp.
|
||||
Der funktioniert ähnlich wie Array-Ranges in Python.
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let s = String::from("hello world");
|
||||
|
||||
let hello = &s[0..5];
|
||||
let world = &s[6..11];
|
||||
----
|
||||
|
||||
Rust kümmert sich dabei darum, dass wir jetzt keinen Unsinn mehr mit `s` machen.
|
||||
Sollte man versuchen `s` zu mutaten und danach die Slice zu nutzen, gibt es einen Fehler, denn Slices sind genauso Referenzen.
|
||||
|
||||
Fun fact: String Literale sind auch Slices und damit Referenzen von Strings.
|
||||
Noch mehr fun fact: Da dynamische String und String Literale damit quasi den selben Typ beschreiben, haben sie auch den gemeinsamen Typ `&str`.
|
||||
Für Leseoperationen kann also im Allgemeinen dieser benutzt werden.
|
||||
|
||||
Slices können auch mutable sein, dafür muss aber das ursprüngliche Array mutable sein und es kann immer nur ein mutable Slice gleichzeitig existieren (also genauso wie beim Ownership).
|
||||
210
src/content/rust/_05 - Structs.adoc
Normal file
210
src/content/rust/_05 - Structs.adoc
Normal file
@@ -0,0 +1,210 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-08-10T17:04:53+02:00
|
||||
|
||||
= Structs
|
||||
|
||||
https://doc.rust-lang.org/book/ch05-00-structs.html[Link zum Buch]
|
||||
|
||||
== Was sind Structs
|
||||
|
||||
Structs kennt man ja aus C/C++.
|
||||
Man kann es (denke ich) auch mit JavaScript Objekten vergleichen.
|
||||
|
||||
In Structs gruppiert man zusammengehöriges Zeug und hat so eine Art Pseudo-OOP.
|
||||
Man kann damit neue Datentypen machen.
|
||||
|
||||
== How to
|
||||
|
||||
=== "Normale" Structs
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
struct User {
|
||||
active: bool,
|
||||
username: String,
|
||||
email: String,
|
||||
sign_in_count: u64,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut user1 = User {
|
||||
email: String::from("someone@example.com"),
|
||||
username: String::from("someusername123"),
|
||||
active: true,
|
||||
sign_in_count: 1,
|
||||
};
|
||||
|
||||
println!("{}", user1.email);
|
||||
|
||||
user1.email = String::from("anotheremail@example.com");
|
||||
}
|
||||
----
|
||||
|
||||
Hinweis: Es können nicht einzelne Felder mutable sein, sondern wenn dann immer das ganze Struct.
|
||||
|
||||
==== Dinge wie in Javascript
|
||||
Wenn die Variable heißt wie das Feld, kann man auch statt `email: email` einfach nur `email` schreiben.
|
||||
|
||||
Wenn man ein neues Struct aus einem alten mit Updates erstellen will, geht das auch mit einer Art Spread-Parameter:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let user2 = User {
|
||||
email: String::from("another@example.com"),
|
||||
..user1
|
||||
};
|
||||
----
|
||||
|
||||
`..user1` *muss* als letztes kommen und füllt dann alle bisher nicht gesetzten Felder.
|
||||
Außerdem ist das etwas tricky:
|
||||
Wenn die Daten, die von `user1` zu `user2` übertragen werden, gemoved werden (sprich: keine primitiven Datentypen sind), dann ist `user1` danach ungültig.
|
||||
Hätten wir jetzt auch noch einen neuen `username` gesetzt (auch ein String) und nur `active` und `sign_in_count` übertragen, wäre `user1` noch gültig.
|
||||
|
||||
=== Tupel Structs
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
struct RGBColor(u8, u8, u8);
|
||||
|
||||
fn main() {
|
||||
let black = Color(0, 0, 0)
|
||||
}
|
||||
----
|
||||
|
||||
Sind nutzbar wie Tupel (destrucuture und `.index` zum Zugriff auf Werte), allerdings eben ein eigener Typ.
|
||||
|
||||
=== Unit-Like Structs
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
struct AlwaysEqual;
|
||||
----
|
||||
|
||||
Ein Struct muss keine Felder haben.
|
||||
Das Buch meint, man könnte für diesen Datentypen jetzt noch Traits implementieren, aber davon habe ich noch keine Ahnung.
|
||||
Nur dann macht diese Art von Struct irgendwie Sinn.
|
||||
|
||||
== Ownership der Felder
|
||||
|
||||
Im ersten Beispiel wird `String` satt `&str` genutzt.
|
||||
Wir wollen am besten im Struct keine Referenzen, oder es müssen "named lifetime parameter" sein, etwas das wir erst später lernen.
|
||||
Der Compiler wird sonst streiken.
|
||||
|
||||
== Das erste Mal Traits
|
||||
|
||||
Im Buch folgt ein Beispielprogramm für ein Struct, das ein Rechteck abbildet.
|
||||
Wir wollten das ganze printen (mit `{}` als Platzhalter), allerdings implementiert Das Rechteck nicht `std::fmt::Display`.
|
||||
Das scheint eine Art `toString()` für Rust zu sein.
|
||||
|
||||
Es gibt aber noch eine andere Möglichkeit und das haben wir schonmal für Tupel genutzt:
|
||||
`{:?}` als Platzhalter (bzw. `{:#?}` für pretty print).
|
||||
Dafür brauchen wir aber das Trait `Debug`.
|
||||
Zum Glück scheint das aber einfach zu implementieren sein, es muss nur implementiert werden.
|
||||
|
||||
Der Compiler schlägt uns zwei Varianten vor:
|
||||
|
||||
1. `#[derive(Debug)]` über der Definition des Structs
|
||||
2. `impl Debug for Rectangle` manuell
|
||||
|
||||
Jetzt können wir Variablen dieses Typs printen und es zeigt uns Datentyp und Felder an.
|
||||
|
||||
Alternativ kann man auch das Makro `dbg!(...)` nutzen.
|
||||
Das wird dann auf `stderr` geprintet.
|
||||
Man kann sogar ein Statement da rein packen (also zum Beispiel `30 * x`) und bekommt das Statement mit dem Ergebnis geprintet, wobei das Ergebnis (als Wert, nicht Referenz) auch zurückgegeben wird.
|
||||
|
||||
== Funktionen in Structs
|
||||
|
||||
Unser Struct soll jetzt auch eine Funktion auf sich selbst aufrufen können.
|
||||
Tatsächlich ist der sehr einfach und sehr OOPig.
|
||||
|
||||
Die folgenden Beispiele sollten relativ viel erklären:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
struct Rectangle {
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
// Das ist eine Methode/"method"
|
||||
// Erster Parameter ist &self (/&mut self) und wird aufgerufen wie folgt:
|
||||
// var.area();
|
||||
fn area(&self) -> u32 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
// Das ist eine "associated function"
|
||||
// Kein &self und aufgerufen wie folgt:
|
||||
// Rectangle::square(5);
|
||||
fn square(size: u32) -> Rectangle {
|
||||
Rectangle {
|
||||
width: size,
|
||||
height: size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mehrere impl Blöcke sind erlaubt
|
||||
impl Rectangle {
|
||||
// var.has_same_area(&other);
|
||||
fn has_same_area(&self, other: &Rectangle) -> bool {
|
||||
self.area() == other.area()
|
||||
}
|
||||
|
||||
// Rectangle::same_area(&first, &second);
|
||||
fn same_area(first: &Rectangle, second: &Rectangle) -> bool {
|
||||
first.area() == second.area()
|
||||
}
|
||||
|
||||
// Methoden können auch wie Felder heißen
|
||||
fn width(&self) -> bool {
|
||||
self.width > 0
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let rect1 = Rectangle {
|
||||
width: 12,
|
||||
height: 3,
|
||||
};
|
||||
let rect2 = Rectangle::square(6);
|
||||
|
||||
println!("{}", rect1.area()); // 36
|
||||
println!("{}", rect2.area()); // 36
|
||||
|
||||
println!("{}", rect1.has_same_area(&rect2)); // true
|
||||
println!("{}", rect2.has_same_area(&rect1)); // true
|
||||
println!("{}", Rectangle::same_area(&rect1, &rect2)); // true
|
||||
}
|
||||
----
|
||||
|
||||
=== `&mut self`
|
||||
|
||||
Eine Methode kann auch `&mut self` als ersten Parameter haben.
|
||||
Dann können auch Felder geschrieben werden. In diesem Fall werden Referenzen aber invalidiert!
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
struct Rectangle {
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl Rectangle {
|
||||
fn change_width(&mut self, width: u32) {
|
||||
self.width = width;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut rect1 = Rectangle {
|
||||
width: 12,
|
||||
height: 3,
|
||||
};
|
||||
let ref1 = &rect1;
|
||||
rect1.change_width(5);
|
||||
|
||||
println!("{}", ref1.width); // <- geht nicht!
|
||||
}
|
||||
----
|
||||
152
src/content/rust/_06 - Enums.adoc
Normal file
152
src/content/rust/_06 - Enums.adoc
Normal file
@@ -0,0 +1,152 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-10-18T17:56:26+02:00
|
||||
|
||||
= Enums und Pattern Matching
|
||||
|
||||
https://doc.rust-lang.org/book/ch06-00-enums.html[Link zum Buch]
|
||||
|
||||
== Enums
|
||||
|
||||
Enumarations gibt's in vielen Programmiersprachen, in Rust scheinen sie aber eine große Rolle einzunehmen.
|
||||
"Enumeration" stimmt eigentlich gar nicht, Enums haben hier nämlich nicht zwangsläufig was mit Zahlen zu tun.
|
||||
Grundsätzlich ist ein "Enum" in Rust näher am "Union" würde ich denken.
|
||||
|
||||
Ein einfaches Beispiel für ist der Typ `Option<T>` (vergleichbar mit Python oder Java `Optional`).
|
||||
Dieser ist entweder `None` oder `Some(value: T)` - es kann also ein Wert zusätzlich zur "Definition" beinhalten.
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
enum Farbcode {
|
||||
Hex,
|
||||
Rgb,
|
||||
}
|
||||
|
||||
let hexcolor = Farbcode::Hex;
|
||||
----
|
||||
|
||||
`Farbcode` ist also ein im Code benutzbarer Datentyp, genauso wie `Farbcode::Hex`.
|
||||
Wenn eine Funktion nun eine Variable mit Typ `Farbcode` erwartet, kann diese Variable sowohl `Hex` oder `Rgb` sein.
|
||||
Die Funktion kann dann je nach Typ verschieden funktionieren.
|
||||
|
||||
Wie schon erwähnt, kann so ein Enum-Wert auch Werte beinhalten, um das zu machen, schreiben wir den Code einfach um:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
enum Farbcode {
|
||||
Hex(String),
|
||||
Rgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
// Alternativ:
|
||||
struct Hex(String);
|
||||
struct Rgb(u8, u8, u8);
|
||||
|
||||
enum Farbcode {
|
||||
Hex,
|
||||
Rgb
|
||||
}
|
||||
|
||||
let hexcode = Farbcode::Hex(String::from("00affe"));
|
||||
let rgbcode = Farbcode::Rgb(125, 255, 255);
|
||||
----
|
||||
|
||||
Natürlich können die Structs jeder Art sein.
|
||||
Enums sind aber auch selber eine Art Struct.
|
||||
Also können wir für Enums auch Methoden definieren wie für Structs.
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
impl Farbcode {
|
||||
fn to_css_string(&self) {
|
||||
// Methode, die für Hex und Rgb angewendet werden kann
|
||||
}
|
||||
}
|
||||
|
||||
let rgbcode = Farbcode::Rgb(125, 255, 255);
|
||||
rgbcode.to_css_string();
|
||||
----
|
||||
|
||||
Tatsächlich ist damit so etwas wie Vererbung implementierbar.
|
||||
Es gibt zwar keine Attribute, aber da ja auch die internen Structs Methoden haben können, ist eine gewisse Hierarchie erstellbar.
|
||||
|
||||
=== `Option<T>`
|
||||
|
||||
Options hab ich oben schonmal kurz beschrieben.
|
||||
In Rust ist dieser Datentyp sehr wichtig.
|
||||
Die Dokumentation dazu ist https://doc.rust-lang.org/std/option/enum.Option.html[hier zu finden] und enthält sehr viel Wichtiges und Interessantes.
|
||||
|
||||
== `match`
|
||||
|
||||
`match` ist quasi das `switch` von Rust.
|
||||
Nur kann es auch prüfen, ob eine Variable einem Enum-Typen angehört.
|
||||
So wie Rust bis jetzt klang, kann wahrscheinlich jedem Datentypen ein "match-Trait" gegeben werden, der dann eine "Zugehörigkeit" (Gleichheit stimmt ja irgendwie nicht) prüfen kann.
|
||||
|
||||
Aber ganz einfach: Angenommen wir wollen die Methode `to_css_string` von oben implementieren.
|
||||
Diese Methode muss ja, je nach Typ, völlig unterschiedlich funktionieren.
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
enum Farbcode {
|
||||
Hex(String),
|
||||
Rgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
impl Farbcode {
|
||||
fn to_css_string(&self) -> String {
|
||||
match self {
|
||||
// format! ist offensichtlich ein Pragma, dass Strings erstellt auf die selbe Weise wie println!
|
||||
Farbcode::Hex(hex) => format!("#{}", hex),
|
||||
Farbcode::Rgb(r, g, b) => format!("rgb({}, {}, {})", r, g, b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let hexcode = Farbcode::Hex(String::from("affe00"));
|
||||
let rgbcode = Farbcode::Rgb(125, 255, 255);
|
||||
|
||||
println!("{}", hexcode.to_css_string());
|
||||
println!("{}", rgbcode.to_css_string());
|
||||
}
|
||||
----
|
||||
|
||||
Hier sieht man auch ganz gut, wie im Match dem "Inhalt" des Enums direkt Namen gegeben werden und Tuples auch dekonstruiert.
|
||||
Im Beispiel ist auch deutlich, dass `match` einen Rückgabewert hat, nämlich das, was im Statement(-Block) des jeweiligen Matches zurückgegeben wird.
|
||||
|
||||
=== Vollständigkeit
|
||||
|
||||
Entweder muss ein `match` eines Enums jede mögliche Variante abgrasen oder es gibt zwei Alternativen.
|
||||
|
||||
`other` ist quasi das `default` von Rust.
|
||||
Aber auch `\_` matched alles.
|
||||
Der Unterschied ist, dass bei `other` noch der Inhalt genutzt werden kann, bei `_` wird er direkt ignoriert und ist nicht nutzbar.
|
||||
|
||||
=== `if let`
|
||||
|
||||
Dieses if-Konstrukt nutzt man am besten, wenn man nur auf eine einzelne Variante eines Enums prüfen möchte.
|
||||
Letztendlich ist es ganz simpel:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
#[derive(Debug)]
|
||||
enum Muenzwurf {
|
||||
Kopf,
|
||||
Zahl,
|
||||
Seite
|
||||
}
|
||||
|
||||
fn print_wurf(ergebnis: Muenzwurf) {
|
||||
if let Muenzwurf::Seite = ergebnis {
|
||||
println!("Das glaub ich nicht! Seite?!");
|
||||
} else {
|
||||
println!("Du hast {:?} geworfen.", ergebnis);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let ergebnis = Muenzwurf::Zahl;
|
||||
print_wurf(ergebnis); // Du hast Zahl geworfen.
|
||||
let ergebnis = Muenzwurf::Seite;
|
||||
print_wurf(ergebnis); // Das glaub ich nicht! Seite?!
|
||||
}
|
||||
----
|
||||
166
src/content/rust/_07 - Management.adoc
Normal file
166
src/content/rust/_07 - Management.adoc
Normal file
@@ -0,0 +1,166 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-10-18T17:56:26+02:00
|
||||
|
||||
= How to: Projektmanagement
|
||||
|
||||
https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html[Link zum Buch]
|
||||
|
||||
== Packages, Crates, Modules, was?
|
||||
|
||||
Rust hat ein sehr hierarchisches Konzept, was die Strukturierung von Projekten angeht.
|
||||
|
||||
Fangen wir mal von oben an:
|
||||
|
||||
=== Packages
|
||||
|
||||
Packages bestehen aus Crates.
|
||||
Sie fassen diese also quasi zusammen und in `Cargo.toml` wird definiert, wie die Crates zu bauen sind.
|
||||
|
||||
Jedes Package, das wir bis jetzt erstellt haben, hatte standardmäßig eine "binary create" (dazu gleich mehr) im generierten Projekt.
|
||||
|
||||
Die Crates können (soweit wie ich das verstanden habe) in beliebigen Ordnern existieren, falls die Crate so heißen soll wie das Package, ist der Standardpfad `src/main.rs` (für binary) bzw. `src/lib.rs` (für library).
|
||||
|
||||
==== Warum mehrere Crates in einem Projekt?
|
||||
|
||||
Einfaches Beispiel: Man hat eine library crate, die Funktionen für einen Webserver bereitstellt.
|
||||
Man kann dann einfach eine binary crate hinzufügen, die eine Referenz-Nutzung abbildet, also direkt ein Beispiel ist.
|
||||
Dies hilft Nutzern direkt und gleichzeitig testet es direkt auch (wobei richtige Tests natürlich anders zu implementieren sind).
|
||||
|
||||
=== Crates
|
||||
|
||||
Creates sind die eigentlichen "Module".
|
||||
Es gibt zwei Arten: binary und library.
|
||||
|
||||
==== Binary Crates
|
||||
|
||||
Diese Crates können zu einer ausführbaren Datei kompiliert werden.
|
||||
Jedes der bisherigen Beispiele, z.B. auch das link:#/diary/rust/3[Higher-Lower-Spiel] sind eine solche binary crate.
|
||||
|
||||
Ihr Merkmal ist vor allem, dass eine `main`-Funktion existiert, die der Einstiegspunkt ist.
|
||||
|
||||
==== Library Crate
|
||||
|
||||
Wie der Name schon sagt, stellt diese Art Crate nur Funktionen zur Verfügung wie eine Bibliothek.
|
||||
|
||||
=== Modules
|
||||
|
||||
Innerhalb einer Crate können Module existieren.
|
||||
Und hier ist auch schon wieder von OOP abgeschaut.
|
||||
Es können nämlich Rusts `private` und `public` hier genutzt werden.
|
||||
|
||||
Im Hauptprogramm kann mit `mod modulname;` das Modul eingebunden werden. Gesucht wird das Modul dann in `./modulname.rs` oder in `./modulname/mod.rs`, wobei letzteres aber aussieht, als wäre es die veraltete Version.
|
||||
|
||||
Zusätzlich kann auch direkt inline ein Modul erstellt werden.
|
||||
Ein Beispiel:
|
||||
|
||||
[source.notCompiling, rust]
|
||||
----
|
||||
mod testmodul {
|
||||
mod nested_modul {
|
||||
fn funktion() {
|
||||
funktion2();
|
||||
}
|
||||
fn funktion2() {
|
||||
println!("Hello World");
|
||||
}
|
||||
}
|
||||
|
||||
mod zweites_modul {
|
||||
fn funktion() {}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Hello world! Geht nicht...
|
||||
crate::testmodul::nested_modul::funktion();
|
||||
}
|
||||
----
|
||||
|
||||
Das funktioniert noch *nicht*.
|
||||
Denn standardmäßig ist alles private, was nicht explizit public ist.
|
||||
Damit wir den obigen Aufruf machen können, muss der Code so aussehen:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
mod testmodul {
|
||||
pub mod nested_modul {
|
||||
pub fn funktion() {
|
||||
funktion2();
|
||||
}
|
||||
fn funktion2() {
|
||||
println!("Hello World");
|
||||
}
|
||||
}
|
||||
|
||||
mod zweites_modul {
|
||||
fn funktion() {}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Hello world!
|
||||
crate::testmodul::nested_modul::funktion();
|
||||
}
|
||||
----
|
||||
|
||||
Nur so kann auf Submodule und Funktionen dieser Module zugegriffen werden.
|
||||
Wie im "normalen" OOP, können aus diesen öffentlichen Funktionen aber dann auch private aufgerufen werden.
|
||||
|
||||
==== Von unten nach oben
|
||||
|
||||
Um aus einem inneren Modul auf das äußere zuzugreifen, kann übrigens `super::...` verwendet werden.
|
||||
|
||||
==== Structs und Enums
|
||||
|
||||
In Modulen können natürlich auch Structs und Enums verwendet werden.
|
||||
|
||||
Bei Structs ist die Besonderheit, dass die einzelnen Attribute auch wieder private oder public sein können.
|
||||
So kann man folgendes machen:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
mod testmodul {
|
||||
pub struct Teststruct {
|
||||
pub oeffentlich: String,
|
||||
privat: String,
|
||||
}
|
||||
|
||||
impl Teststruct {
|
||||
pub fn generator(wert: &str) -> Teststruct {
|
||||
Teststruct {
|
||||
oeffentlich: String::from(wert),
|
||||
privat: String::from("Sehr geheimer Wert"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a = crate::testmodul::Teststruct::generator("Irgendein Wert");
|
||||
// Geht
|
||||
println!("Öffentlich: {}", a.oeffentlich);
|
||||
// Geht nicht!
|
||||
// println!("Privat: {}", a.privat);
|
||||
}
|
||||
----
|
||||
|
||||
Dagegen gilt für Enums: Wenn der Enum public ist, sind auch alle Varianten public.
|
||||
|
||||
==== Abkürzungen mit `use`
|
||||
|
||||
Angenommen, wir haben eine Mediathek mit Filmen, Serien, Spielen, etc. und brauchen immer lange Zugriffspfade (also z.B. `crate::medien::spiele::liste::add()`), obwohl wir nur Spiele brauchen, kann `use` benutzt werden.
|
||||
|
||||
Wenn wir also `use crate::medien::spiele;` in unseren Code einfügen, können alle diese Befehle verkürzt werden auf eben z.B. `spiele::liste::add()`.
|
||||
Theoretisch können wir das bis hin zu einzelnen Funktionsnamen machen, `se crate::medien::spiele::liste:add;`, würde `add()` im Scope verfügbar machen.
|
||||
|
||||
Dabei gibt es zwei Hinweise:
|
||||
|
||||
1. Es funktioniert nur, wenn sich zwei Namespaces nicht überschneiden. Ein Zufügen von `use andere::mod::add;` geht also nicht!
|
||||
2. Das ganze gilt nur in genau diesem Scope. Falls wir jetzt ein weiteres Modul definieren, können wir darin nicht die Pfade kürzen.
|
||||
|
||||
Und für beides gibt es Umwege:
|
||||
|
||||
1. Man kann `use andere::mod::add as modAdd;` benutzen.
|
||||
2. Sollten wir `pub use ...` benutzen, kann tatsächlich diese Abkürzung benutzt werden.
|
||||
|
||||
`pub use` kann auch benutzt werden, alle möglichen Module in seiner Crate miteinander reden zu lassen, aber nach außen nur bestimmte Schnittstellen freizugeben.
|
||||
239
src/content/rust/_08 - Collections.adoc
Normal file
239
src/content/rust/_08 - Collections.adoc
Normal file
@@ -0,0 +1,239 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-10-18T17:56:27+02:00
|
||||
|
||||
= Standard Collections
|
||||
|
||||
https://doc.rust-lang.org/book/ch08-00-common-collections.html[Link zum Buch]
|
||||
|
||||
== `Vec<T>` - Vektoren
|
||||
|
||||
Vektoren kennt man ja aus C++ als dynamische Alternative zu Arrays.
|
||||
Es ist quasi eine Linked List, die beliebig erweiterbar bzw. manipulierbar ist.
|
||||
Wie in der Überschrift zu sehen, sind sie typspezifisch, man kann also nur Daten eines einzigen Typs in diese Liste speichern.
|
||||
|
||||
Wie benutze ich jetzt so einen Vector?
|
||||
Hier einfach mal eine Übersicht:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
// -- Erstellen --
|
||||
// Mit dem vec!-Pragma
|
||||
let v = vec![1, 2, 3];
|
||||
let v = vec![0; 5]; // 5 Nullen
|
||||
// Mit new() (mit mut, damit wir gleich etwas zufügen können)
|
||||
let mut v: Vec<i32> = Vec::new();
|
||||
|
||||
// -- Updaten --
|
||||
// Push
|
||||
v.push(1);
|
||||
v.push(2);
|
||||
v.push(3);
|
||||
v.push(4);
|
||||
v.push(5);
|
||||
// -> [1,2,3,4,5]
|
||||
// Pop, returnt ein Optional<T>
|
||||
v.pop(3);
|
||||
v.pop(4);
|
||||
// -> [1,2,3]
|
||||
// Insert
|
||||
v.insert(1, 9); // -> [1,9,2,3]
|
||||
// Remove
|
||||
v.remove(1); // -> [1,2,3]
|
||||
|
||||
// -- Lesen --
|
||||
// Über Index
|
||||
let second: &i32 = &v[1];
|
||||
// Mit get() (gibt ein Option<&T>)
|
||||
// Hat den Vorteil, dass es nicht einfach paniced.
|
||||
match v.get(2) {
|
||||
Some(value) => {...}
|
||||
None => (),
|
||||
}
|
||||
|
||||
// -- Drüber iterieren --
|
||||
// mut natürlich nur, wenn wir es verändern wollen
|
||||
// Wir brauchen hier aber * zum Dereferenzieren!
|
||||
for i in &mut v {
|
||||
*i += 50;
|
||||
}
|
||||
----
|
||||
|
||||
=== Achtung, Scope
|
||||
|
||||
Wenn ein Vector aus dem Scope fällt, wird er zusammen mit seinem Inhalt gedropped.
|
||||
Blöd, wenn man Referenzen auf Elemente aus dem Vector hat.
|
||||
|
||||
=== Ownership
|
||||
|
||||
Wenn `push()` ausgeführt wird, findet ein mutable borrow statt und das kommt mit allen Eigenheiten wie vorher.
|
||||
Alle Referenzen, die vorher über Index oder `get()` genommen wurden, sind dann ungültig.
|
||||
Das liegt daran, dass es by `push()` passieren kann, dass neue Speicher reserviert und genutzt werden muss, falls die Elemente nicht mehr nebeneinander passen.
|
||||
|
||||
=== Lifehack: Enum für verschiedene Datentypen
|
||||
|
||||
Ein Vector kann nur einen Datentypen aufnehmen?
|
||||
Der Datentyp kann aber auch ein Enum sein!
|
||||
|
||||
Also wenn mal ein String neben Zahlen gespeichert werden soll: Einfach einen Enum mit beiden Varianten anlegen.
|
||||
|
||||
=== Weiteres
|
||||
|
||||
Es gibt auch hier Slices und noch eine Menge Tricks.
|
||||
Die https://doc.rust-lang.org/std/vec/struct.Vec.html[Dokumentation zum Vector] ist da wahrscheinlich sehr hilfreich.
|
||||
|
||||
== Strings
|
||||
|
||||
Strings eine Collection?
|
||||
Klar, wie in C ja auch.
|
||||
|
||||
Es gibt im Core eigentlich nur `str`, also ein Slice.
|
||||
Der `String`-Typ kommt aus der Standard-Lib und ist einfacher zu nutzen.
|
||||
|
||||
In den meisten Programmiersprachen kennt man ja `toString()`, hier ist es natürlich `to_string()` und für alle Typen definiert, die den Trait `Display` implementiert haben.
|
||||
Das gilt zum Beispiel auch für String-Literale, man kann also `str` ganz einfach in einen `String` umwandeln, indem man `"text".to_string()` aufruft.
|
||||
Natürlich funktioniert auch `String::from("text")`.
|
||||
|
||||
String sind UTF-8 encoded, also egal mit was man sie bewirft, es sollte klappen.
|
||||
Allerdings ist das Handling deshalb etwas kompliziert.
|
||||
Rust fasst das ganz gut am Ende der Seite zusammen mit
|
||||
[quote]
|
||||
To summarize, strings are complicated.
|
||||
|
||||
Hier wieder eine Übersicht zur Nutzung:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
// -- Erstellen --
|
||||
// String::from()
|
||||
// "Hello ".to_string() macht das selbe
|
||||
let mut s = String::from("Hello ");
|
||||
|
||||
// -- Manipulieren --
|
||||
// push_str()
|
||||
// Hier wird kein Ownership übergeben!
|
||||
// Sollte "world" in einer Variable stehen, ist sie danach weiter nutzbar.
|
||||
s.push_str("world");
|
||||
// -> Hello World
|
||||
// push(), für einen einzelnen Character
|
||||
s.push('!');
|
||||
// +
|
||||
// Ist etwas tricky. Der Methodenkopf sieht so aus:
|
||||
// fn add(self, s: &str) -> String
|
||||
// Also wird Ownership von s1 übergeben und s2 offensichtlich magisch von &String zu &str.
|
||||
// Somit ist danach auch s1 nicht mehr gültig.
|
||||
let s1 = String::from("Hello ");
|
||||
let s2 = String::from("world!");
|
||||
let s3 = s1 + &s2;
|
||||
// Es geht auch mit noch mehr Elementen!
|
||||
// Damit das aber nicht zu unübersichtlich wird, gibt es format!
|
||||
let s1 = String::from("Schere");
|
||||
let s2 = String::from("Stein");
|
||||
let s3 = String::from("Papier");
|
||||
let s4 = format!("{}, {}, {}", s1, s2, s3);
|
||||
// Hier wird kein Ownership übergeben!
|
||||
----
|
||||
|
||||
=== Indexing
|
||||
|
||||
Aus Python z.B. kennt man ja `"Hallo"[0] -> H`.
|
||||
In Rust geht das nicht.
|
||||
Das liegt am Aufbau der String, dass sie eben UTF-8 verwenden und `String` eigentlich nur ein `Vec<u8>` ist.
|
||||
Das macht das ganze ordentlich schwierig.
|
||||
|
||||
=== Slicing
|
||||
|
||||
Ist immer eine schlechte Idee, außer man weiß exakt wie lang die einzelnen Zeichen (in Byte) des Strings sind.
|
||||
Im Englischen ist es normalerweise 1 Byte pro Zeichen, Umlaute sind schon 2, und so weiter.
|
||||
Sollte man aus Versehen ein Zeichen "durchschneiden" (also nur 1 Byte eines "ü" im Slice haben), gibt es eine Runtime Panic.
|
||||
|
||||
=== Iterieren
|
||||
|
||||
Über einem String iterieren geht ganz ok.
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
for c in "hallo".chars() {
|
||||
println!("{}", c);
|
||||
}
|
||||
// Ist für europäische Sprachen absolut geeignet.
|
||||
// Bei Hindi wird es schon wieder eklig.
|
||||
|
||||
for b in "hallo".bytes() {
|
||||
println!("{}", b);
|
||||
}
|
||||
// Wirft eben die einzelnen u8 raus.
|
||||
----
|
||||
|
||||
Wenn wir "grapheme" haben wollen (Was anscheinend so etwas wie "volle Zeichen" sind, mehr als nur char), gibt es keine eingebaute Funktion aber crates, die das lösen.
|
||||
|
||||
== HashMaps
|
||||
|
||||
Der Erlöser der Programmierer und Lösung jeder Aufgabe bei der Bewerbung, die "O(n)" enthält.
|
||||
Oder so ähnlich.
|
||||
|
||||
Nutzung:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
// Das hier ist für die "Abkürzungen"
|
||||
use std::collections::HashMap;
|
||||
|
||||
// -- Erstellen --
|
||||
// iter(), zip() und collect()
|
||||
// collect() kann in alles mögliche wandeln, deshalb muss der Typ angegeben werden.
|
||||
let woerter = vec![String::from("eins"), String::from("zwei"), String::from("drei")];
|
||||
let zahlen = vec![1, 2, 3];
|
||||
let mut zahlwort: HashMap<_, _> = woerter.into_iter().zip(zahlen.into_iter()).collect();
|
||||
// Einfach normal
|
||||
let mut zahlwort = HashMap::new();
|
||||
|
||||
// -- Nutzung --
|
||||
// insert()
|
||||
// Ownership wird bei den Strings übergeben
|
||||
zahlwort.insert(String::from("eins"), 1);
|
||||
zahlwort.insert(String::from("zwei"), 2);
|
||||
zahlwort.insert(String::from("drei"), 5);
|
||||
zahlwort.insert(String::from("drei"), 3); // Überschreibt vorheriges
|
||||
// get()
|
||||
// Hier wird kein Ownership übergeben
|
||||
let testwort = String::from("eins");
|
||||
let eins_oder_none = zahlwort.get(&testwort); // -> Optional
|
||||
// entry()
|
||||
// Checkt, ob etwas da ist und kann im Zweifel etwas einfügen
|
||||
zahlwort.entry(String::from("vier")).or_insert(4);
|
||||
// entry kann auch genutzt werden, um den bisherigen Eintrag upzudaten
|
||||
let bisher = zahlwort.entry(String::from("vier")).or_insert(4); // &mut i32
|
||||
*bisher += 1;
|
||||
|
||||
// Drüber Iterieren
|
||||
for (key, value) in &zahlwort {
|
||||
println!("{}: {}", key, value);
|
||||
}
|
||||
// Sehr selbsterklärend
|
||||
----
|
||||
|
||||
=== Ownership
|
||||
|
||||
Falls Key oder Value kein Copy Trait haben, wird der Ownership übergeben. Strings sind also danach ungültig.
|
||||
|
||||
== Hausaufgaben
|
||||
|
||||
Das Buch gibt uns hier ein paar Aufgaben, die wir jetzt lösen können:
|
||||
|
||||
* Den Median aus einer Liste finden. Erst sortieren, dann den mittleren Wert.
|
||||
* Wörter zu "pig-latin" machen. Wenn erster Buchstabe ein Vokal ist, wird "-hay" angehängt, wenn es ein Konsonant ist, wird er ans Ende angefügt (nach "-") und "ay" angehängt.
|
||||
* Eine kleine Befehlszeile mit Befehlen wie "Add Name to Sales" und Ausgabe.
|
||||
|
||||
Vielleicht werde ich sie irgendwann mal lösen, dann landet der Code hier.
|
||||
|
||||
=== Aufgabe 1
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
fn main() {
|
||||
let mut list = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
list.sort();
|
||||
let mid = list.len() / 2; // integer divide
|
||||
println!("{}", list[mid]);
|
||||
}
|
||||
----
|
||||
104
src/content/rust/_09 - Errors und panic.adoc
Normal file
104
src/content/rust/_09 - Errors und panic.adoc
Normal file
@@ -0,0 +1,104 @@
|
||||
:experimental:
|
||||
:docdatetime: 2022-08-22T17:04:01+02:00
|
||||
|
||||
= Errors und `panic!`
|
||||
|
||||
https://doc.rust-lang.org/book/ch09-00-error-handling.html[Link zum Buch]
|
||||
|
||||
== `panic!`
|
||||
|
||||
Dieses Makro it furchtbar simpel: Es macht Panik und das Programm stirbt mit einem Fehler.
|
||||
Diesen Fehler kann man auch nicht catchen.
|
||||
|
||||
Wenn `RUST_BACKTRACE` als Umgebungsvariable gesetzt ist, wird auch noch ein langer Traceback angezeigt, allerdings nur, solange Debug-Symbole aktiviert sind (also bei `cargo run` oder `cargo build` ohne `--release`).
|
||||
|
||||
Will man gar kein Traceback und kein "unwinding" (das "hochgehen" durch den Funktionsstack und Aufräumen), kann man auch noch folgendes zu seiner `Cargo.toml` hinzufügen:
|
||||
|
||||
[source, toml]
|
||||
----
|
||||
[profile.release]
|
||||
panic = 'abort'
|
||||
----
|
||||
|
||||
== `Result<T, E>`
|
||||
|
||||
Der Result-Datentyp ist deutlich besser für mögliche Fehler geeignet, die das Programm abfangen und bearbeiten kann.
|
||||
Falls zum Beispiel eine Datei auf dem Dateisystem nicht existiert, ist es ja manchmal gewünscht, dass diese Datei dann einfach angelegt wird.
|
||||
|
||||
Der `Result`-Typ ist ein Enum von `Ok<T>` und `Error<E>`.
|
||||
Also kann dann mit `match` geprüft werden, was genau wir gerade bekommen haben.
|
||||
Alternativ können auch Funktionen wie `unwrap_or_else(|error| {...})` genutzt werden.
|
||||
|
||||
`Ok<T>` verhält sich wie `Some<T>` und sollte zurückgegeben werden, wenn alles glatt läuft.
|
||||
|
||||
`Error<E>` beinhaltet einen Fehler.
|
||||
Der genaue Fehler kann mit `error.kind()` erfahren werden; ein weiteres `match` ist dann eine "genauere" Fehlerbehandlung.
|
||||
|
||||
Ein volles Beispiel mit ganz viel `match`:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
use std::fs::File;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
fn main() {
|
||||
let greeting_file_result = File::open("hello.txt");
|
||||
|
||||
let greeting_file = match greeting_file_result {
|
||||
Ok(file) => file,
|
||||
Err(error) => match error.kind() {
|
||||
ErrorKind::NotFound => match File::create("hello.txt") {
|
||||
Ok(fc) => fc,
|
||||
Err(e) => panic!("Problem creating the file: {:?}", e),
|
||||
},
|
||||
other_error => {
|
||||
panic!("Problem opening the file: {:?}", other_error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
=== `unwrap()` und `expect()`
|
||||
|
||||
Machen aus einem `Result<T, E>` entweder ein `T` oder eine `panic!`.
|
||||
Bei `expect()` kann man noch die Fehlermeldung festlegen.
|
||||
|
||||
Warum man jemals `unwrap()` nehmen sollte, erschließt sich mir nicht ganz.
|
||||
|
||||
=== `?`
|
||||
|
||||
Oft schreibt man Funktionen so, dass Fehler weiter "hochgegeben" werden, falls man welche bekommt.
|
||||
`?` macht genau das bei einem Result.
|
||||
Codemäßig erklärt:
|
||||
|
||||
[source, rust]
|
||||
----
|
||||
let a = match result {
|
||||
Ok(nummer) => nummer,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// Ergibt das selbe wie
|
||||
|
||||
let a = result?;
|
||||
----
|
||||
|
||||
Das `?` kann auch für zum Beispiel `Option` verwendet werden, dann returned es natürlich `None`.
|
||||
|
||||
=== Rückgaben von `main()`
|
||||
|
||||
Bis jetzt hat `main()` immer nichts, also implizit `()` zurückgegeben.
|
||||
Manchmal wollen wir ja aber auch was anderes als "0" als return code haben.
|
||||
Wir können Tatsächlich auch ein Result zurückgeben. Und zwar ein `Result<(), Box<dyn Error>>`.
|
||||
Der zweite Typ dort, kann wohl als "irgendein Fehler" gelesen werden und wird später noch erklärt.
|
||||
|
||||
Allgemein kann aber jedes Objekt, dass `std::process::Termination`-Trait implementiert von main als Rückgabe genutzt werden.
|
||||
|
||||
== Wann `Result<T, E>`, wann `panic!`?
|
||||
|
||||
Der Artikel ist sehr sehr sehr lang, aber eigentlich sagt er:
|
||||
"Panic nur wenn es eben nicht gerettet werden kann."
|
||||
Und obviously in Tests.
|
||||
|
||||
Und man kann natürlich auch tolle eigene Fehlertypen für Result bauen.
|
||||
Reference in New Issue
Block a user