Rust-Grundlagen lernen

Beschreibung

Generelle Rust-Grundlagen und Konzepte lernen. Auch, wie man Cargo bedient.

Ziel

Das jeder der Teilgenommen hat ungefähr weiß wie man Rust benutzt.

Art der Session

Experimentiersession

Nützliches Material

Vorgehen

Voraussetzungen

Lernziel

Zeit und Ort

Teilnehmende

Wer hat Interesse?

  • Ja
  • Vielleicht
0 Teilnehmer

Bereits erledigt

Aufsetzen

  1. Wie installiert man Rust
    Man installiert Rust mit rustup wie auf der Rustlang-Website spezifiziert, oder alternativ über das Debian-Paket
  2. Man kann wenn man möchte auch auf hier den Rust analyser LSP zu seinem Texteditor hinzufügen

Erste Schritte

Hallo Welt

Um ein Hallo Welt Programm zu erstellen muss man eine Datei mit der .rs Endung erstellen und diesen Code einfügen:

fn main() {
    println!("Hallo, Welt");
}
Die Main-Funktion
fn main() { //snip }

Die main Funktion wird beim Starten den Programms ausgeführt und geladen. Sie ist der Einstiegspunkt ins Programm.

Das println!() Makro
println!("Hallo, Welt");

println! (=print line) ist ein Makro, welches einen formatierten String über den Standard-Output in die Konsole printed. Es ist vergleichbar mit print in python.

Ein Makro ist ein Stück Code, welches bei Compile-Time expandiert und ausgefüllt wird. Dies macht den Code lesbarer. Ein Makro hat immer ein ! am Ende.

Ausführung des Programmes

Um unser Hallo-Welt-Programm auszuführen müssen wir die Code-Datei compilieren. Dies machen wir einfach mit dem Befehl rustc (= Rust-Compiler). Dann können wir die binäre Datei, die der Compiler zurück gibt ausführen. Das sieht dann so aus:

rustc filename.rs
./filename

Cargo-Basics

Cargo ist der offizielle Paket-Manager für Rust und das offizielle Build-Tool.
Um mit Cargo ein neues Projekt zu erstellen muss man einfach cargo new <Projektname> ausführen und Cargo erstellt ein neues VCS-Verzeichnis (standardmäßig mit git), einem src ordner, wo die Rust-Code-Dateien gespeichert werden, einer Cargo.toml, welche die Abhängigkeiten und die Aktuelle Paketversion enthält, einer Cargo.lock in der Eine Versionsgeschichte gespeichert wird und dem target Verzeichnis, wo Cargo die Ausführbaren Dateien speichert.

Um das Programm mit Cargo zu Compilieren gibt es den einfachen Befehl cargo build und um das Programm zu Compilieren und auszuführen cargo run. Außerdem gibt es noch den Befehl cargo clippy, welcher dir Feedback zu deinem Code gibt und mit schnell überprüft, ob der Code überhaupt compilieren würde.

Außerdem gibt es den cargo doc (cargo doc --open) Befehl, welcher eine Dokumentation für den Code anhand von Dokumentierungs-Kommentaren (///), in denen man Markdown schreiben kann, generiert und mit der --open Flagge im Browser öffnet.

Mutable und Immutable

Variablen in Rust sind standardmäßig immutable, das heißt sie können nicht nach der Zuweisung geändert werden, dies kann man bei der Deklaration mit dem mut-Keyword ändern, welches die Variable mutable/änderbar macht.

let a = 12; // immutable i32 a
a += 2; // Fehler, a ist immutabel und kann nicht verändert werden
let mut b = 12; // mutable i32 b
b += 2; // Ok, b ist mutabel und darf verändert werden.

Jedoch kann man immutable Variablen ändern, indem man sie neu zuweist, auch shadowed. Diese Aktion braucht jedoch mehr Leistung, als die Variable zu ändern, deswegen benutzt man mutable Variablen, wenn man weiß, dass sich der Wert ändern wird.

let a = 12;
let a = a + 12; // Ok, da a neu zugewiesen wird.

Normale Variabeltypen

Ganzzahlen
Signed Größe Unsigned
i8 1 byte u8
i16 2 byte u16
i32 4 byte (standard) u32
i64 8 byte u64
i128 16 byte u128
isize Architektur usize
Kommazahlen / Floatingpoints

Standard f64 = signed 64 bit Präzision Kommazahl
f32 = signed 32bit

Boolean

Der Boolean wird mit bool festgelegt und kann true oder false sein. Er ist trotzdem 1 Byte groß.

Buchstabe

Der Buchstabentyp akzeptiert einen einzelnen UTF-8 Unicode Buchstaben.

let a: char = 'Z'; // Es müssen einfache Anführungszeichen sein
let b: char = '😻';
Tuple

Ein Tuple ist eine Sammlung von unterschiedlichen Datentypen.

let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
Array

Ein Array ist ein Block Arbeitsspeicher mit aufeinander folgenden Daten des selben Typen.

let a = [1, 2, 3, 4, 5];
let b: [i32; 5] = [1, 2, 3, 4, 5]; 
// a und b sind effektiv gleich

let c = [1, 1, 1, 1, 1];
let d = [1; 5];
let e = [1i32; 5];
// c, d und e sind effektiv gleich

Wichtig: Wenn man versucht auf ein Element außerhalb eines Arrays zuzugreifen wird das Programm paniken und beendet werden

Sessioncode

use std::io::stdin;

/// Struct for a house with the number of inhabitants and the name of the House
// Sodass man sich die Variable ausgeben lassen kann, fügt standard print Verhalten
// hinzu.
#[derive(Debug)] 
struct House {
    inhabitants: i32, // 32 bit signed integer
    name: String, // string auf dem heap
}

impl House {
    /// Creates a new House struct when given a number of inhabitants and 
    /// a string as the name of the house
    fn new(inhabitants: i32, name: String) -> Self {
        House { inhabitants, name } // Ausdruck: Gibt ein house mit inhabitants und
                                    //           name zurück.
    }
}

/// Adds two numbers a and b together
// Funktion, welche 2 32 bit integer, a und b, als Argumente nimmt und einen 32 bit 
// Integer, a + b zurück gibt,  spezifiziert mit `-> i32`
fn add(a: i32, b: i32) -> i32 {
    a + b 
}

/// Asks the the question to the user and Returns an OK 32-bit Integer Result or 
/// an Err string literal result: "You didn't input an i32"
// Gibt einen Ok() und einen Err() zurück, beides Varianten des Result enums
fn input_int(question: &str) -> Result<i32, &str> {
    println!("{question}");
    let mut input = String::new();
    
    // Ließt die vom Nutzer eingegebene Zeile ein, durch ein Enter spezifiziert
    // und falls das nicht geht, was ein Betriebssystemfehler sein muss,
    // wird das Programm mit dem Error "Failed to read line" beendet
    stdin().read_line(&mut input).expect("Failed to read line"); 

    // Die Funktionskette input.trim().parse() entfernt den überflüßigen Whitespace, 
    // wie den Newline-Buchtaben \n, vom String Input und versucht den Input in einen
    // i32 zu übersetzen, da wir festgelegt haben das unsere Funktion im Fall Ok(), 
    // einen i32 zurückgibt, also wenn .parse() erfolgreich ist, wird Ok(num) 
    // zurückgegeben 
    if let Ok(num) = input.trim().parse() {
        Ok(num);
    } 
    // Wenn die .parse nicht erfolgreich ist wird der Fehlertext
    // You didn't input an integer zurückgegeben, das zwingt den Code, der die 
    // Funktion ruft, den möglichen Fehler zu beachten.
    else {
        Err("You didn't input an integer")
    }
}

fn main() {
    // a wird ein vom Nutzer eingegebener Wert zugewiesen
    // Der Nutzer wird solange etwas eingeben müssen, bis er einen gültigen i32 
    // eingibt
    // loop = while True
    let a = loop {
        match input_int("Input an integer") {

            // Beendet die Schleife und gibt den i32, von der input Funktion, zurück.
            Ok(num) => break num,
            // Wirft den Fehler weg und startet die Schleife von neuem
            Err(_) => continue,
        }
    };

    let b = loop {
        // Kurzsyntax für das match oben
        if let Ok(num) = input_int("Input a second integer") {
            break num;
        }
    };

    let c = add(a, b); // c = a + b Funktion oben

    // Formatiert den String, wo die {} Platzhalter für Variablen sind
    println!("You put in {a} and {b}\n{a} + {b} = {c}"); 

    if c < 30 {
        println!("You failed");
    } else {
        println!("You won");
    }

    while c < 30 {
        println!("You failed");
        break;
    }

    // Liste von 0 bis 3 inklusive = 4 Wiederholungen, wobei i 
    // jedes mal neu zugewiesen wird
    for i in 0..=3 {
        println!("{i}");
    }

    // Siehe House oben
    let house = House::new(12, String::from("Darius"));
    // Print house, mit debug Format, da eigener Typ
    println!("{:?}", house);
}

Nächsten Schritte

Was passiert als nächstes? Was sind die nächsten Aufgaben?

  1. Ein Exkurs in C
  • generische Datentypen
  • Enums
  • Burrow-Checker ( Besitztum, Verleih, Lebenszeiten )
  • Error Handling
  • Option<T>
  • Vektoren, Strings und UTF-8
  • Hashmaps (Dictionaries)
  • Pakete, Crates und Module
  • Traits
  • Closures
  • Iteratoren
  • Smart Pointers

Organisatorische Anforderungen

Technische Anforderungen

Didaktische Anforderungen

Pädagogische Anforderungen

Sessionredaktion nötig?

Ablauf des 03.03.2024

  1. Wiederhohlung des Vorherigen Inhaltes

Neue Inhalte

Enumerations (enums)

Enums in Rust sind ähnlich zu enums wie C, jedoch sehr viel funktionaler. Ein normales C-style Enum sieht so aus:

enum ABC {
    A,
    B,
    C
}

Das Enum, namens ABC hat die 3 Varianten A, B und C. Dies sind die einzig möglichen Werte, welche ein Wert dieses Datentypens haben kann.

Aber das ist doch LANGWEILIG

Enums in Rust ermöglichen es sehr einfach Tagged-Unions zu realisieren. Eine Tagged-Union bezeichnet einen Typen, wessen Varianten unterschiedliche assoziierte Typen haben. Eine Tagged-Union ist so groß, wie das größte Element, welches sie enthält. Enumerationen in Rust fallen generell unter den Begriff sum type.

enum ABC {
    A(i32),
    B { a: u32, b: i16 },
    C(u32, u32)
}

Dieses angepasste Enum ABC sagt aus, dass Variante A einen i32, dass B einen u32 namens a und einen i16 namens b und dass C einen Tuple aus zwei u32 enthält.

Beispiele

let a = ABC::A(10);
let b = ABC::B { a: 10, b: 10 };
let c = ABC::C(0xFFFFFFFF, 0x00000000);

Destrukturierungen

Damit man tatsächlich auf diese enthaltenen Werte zugreifen kann, muss man die Enumeration destrukturieren um Typsicherheit zu gewährleisten, da sich Varianten von Unions den selben speicher teilen. Wenn man nähmlich versucht eine Kommazahl zu lesen, wo im speicher eine Ganzzahl liegt läuft man in Probleme.

Jetzt wissen wir, warum wir Enumerationen anständig destrukturieren müssen, aber wie tuen wir das nun?

Option 1: Match

Das match-Keyword erinnert an das match aus Python oder dem C-ähnlichen switch. Bei einem Match muss man daran denken, alle möglichen Werte abzudecken, der Compiler kontrolliert dies.

let var: i8 = 10;
match var {
    0 => { todo!() },       // Falls var == 0
    10 => { todo!() },      // Falls var == 10
    11..20 => { todo!() },  // Falls var ∈ { 10, 11, ... 19 }
    20..=30 => { todo!() }, // Falls var ∈ { 20, 21, ... 30 }
    _ => { todo!() },       // Alles andere
}

Wie destrukturiert man nun eine Enumeration damit? – Ganz einfach:

let a = ABC::A(10);
match a {
    ABC::A(num) => { println!("{num}") }, // num nimmt den Wert im Container a an
    ABC::B(_) => { todo!() },             // unterstrich ignoriert den Wert
    _ => { todo!() }
}

Diese struktur auszuschreiben kann jedoch lästig werden, deswegen gibt es noch ein Paar andere möglichkeiten.

let a = ABC::A(10);
if let ABC::A(num) = a { // falls a ein A ist speichert num den Wert in a und de if bedingung ist war
    todo!() // Dieser Branch wird also betreten.
}

Weitere Informationen:

Aufgabe

  1. Es gibt statisch viele unterschiedliche Tiere x
  2. Der Nutzer gibt ein Wie viele Tiere er haben möchte (maximal statisch y)
  3. Der Nutzer gibt den Typ jedes Tieres was er haben möchte ein, wobei Typ ∈ x

Tipps:

Bitte nach dem Python-Lernen-Workshop mit @lg-didaktik zusammensetzen zur Vorbereitung