In dem Vortrag werde ich das Programm Sudoku-Solver besprechen. Es gibt zwar Lösungsalgorithmen für Sudoku bis hin zu einfachem Brute-Force mit Backtracking, das trotz der 7⋅10²⁷ schnelle Ergebnisse liefert, aber ich wollte die menschlichen Strategien nachprogrammieren.

Code-Struktur

  • Hauptdatei bei Programmen (cargo new --bin) immer src/main.rs, bei Bibliotheken (cargo new --lib) immer src/lib.rs
  • Code kann in eigene Module ausgelagert werden, die eigenen Zugriffsbereich definieren.
    • Nur mit pub veröffentlichte Funktionen, Strukturen, Typen u. s. w. sind von außerhalb erreichbar.
    • Innere Module haben auf alle äußeren Vollzugriff.
  • Module können in Dateien ausgelagert werden und müssen dann mit mod xy; eingebunden werden.
    • Datei muss xy.rs heißen.
    • Wenn Module wiederum Module verwenden, müssen diese in Verzeichnissen liegen mod xy { mod yz { … } } liegt in Datei xy/yz.rs
    • xy.rs und xy/mod.rs sind äquivalent, ich bevorzuge xy/mod.rs
  • eingebettete Module sind für Tests nützlich
  • mit use … werden die Symbole in den aktuellen Bereich importiert; kann auch innerhalb von Funktionen oder Blöcken passieren
    • Bibliotheken und Module definieren solche Bereiche use std::io
    • spezieller Pfad use super:: bezieht sich auf das übergeordnete Modul
    • spezieller Pfad use crate:: bezieht sich auf die Wurzel des Programms, wenn man (in Makros) nicht weiß, wo man ist
    • spezielles Element use …::* für alles
    • spezielles Element use …::self für das Modul selbst, hilfreich bei use std::io::{self, Read}
    • auch Zustände aus Enums können importiert werden
    • aus Importen kann wieder importiert werden
  • Funktionen und Datentypen können auch innerhalb von Funktionen definiert werden; Sichtbarkeitsbereich dementsprechend eingeschränkt; manchmal sehr hilfreich für Hilfsfunktionen
  • grundsätzlich kann eine Struktur/Funktion/… vor ihrer Definition verwendet werden

Ein-/Ausgabe

  • Kommandozeilenargumente mit clap parsen
  • Ausgabe:
    • auf stdout mit print!() oder println!(); ln fügt Zeilenumbruch am Ende hinzu
    • auf stderr mit eprint!() und eprintln!()
    • erstes Argument ist Formatierungsmuster, wobei {} sich auf die Parameter bezieht
    • Formatierung innerhalb von {} auch veränderbar, z. B. {:2X} für zweistellig und hexadezimal in Großbuchstaben oder {:-<5} für links ausgerichtet mit 5 Stellen und - als Füllzeichen; Formatdefinition in std::fmt
    • typisch ist auch {:?} für Debug-Formatierung, Path unterstützt z. B. nur diese
    • Makros, die beim Compilieren die Formatanweisungen auseinander nehmen
    • Ergebnisse der Makros kann man sich mit cargo expand (zuvor cargo install cargo-expand) ansehen
  • formatierte Ausgabe in String mit format!("…", …)
  • zum Schreiben in Dateien gibt es writeln!(file, "…", …)?

Geltungsbereich von Variablen

  • Variablen haben einen Bereich, in dem sie genutzt werden können: Beginn bei let und Ende am Ende des Blocks oder wenn sie an eine Funktion übergeben werden
  • am Ende eines Geltungsbereichs wird der Destruktor aufgerufen, d. h. der Speicher freigegeben
  • künstliche Blöcke mit { … }, Vorteil: optisch erkennbar, Nachteil: größere Einrückung
  • Ende mit drop(var), Variable wird an drop überführt, die damit nichts macht und am Ende der Funktion wiederum ihr Geltungsbereich endet und somit der Variablenwert vernichtet wird
  • Variablenwert lebt weiter, auch wenn er durch Neudeklaration der Variablen nicht mehr erreichbar ⟹ Gefühl von Duck-typing wie in Skriptsprachen

    Benutzung des Datentyps Enum

  • struct sind Datentypen mit mehreren Komponenten

  • enum sind Datentypen mit mehreren Zuständen, aber jede Variable hat zu jedem Zeitpunkt nur einen Zustand
  • Zustände können auch Werte haben; Größe des Datenobjekts ist die Größe des größten Zustands
  • enum äquivalent in C zu einer struct { zustand; union { … } }
  • Beispiele: Option { Some(val), None } und Result { Ok(val), Err(err) }
  • in C hat man Magic-Values wie NULL oder -1 missbraucht, um zwischen Some und None zu unterscheiden
  • Fehler werden in C über errno weitergegeben, was für Parallelität eine Katastrophe ist
  • Zustandabfrage mit Pattern matching: match var { Zustand => { … }, _ => () } oder if let Zustand = var { … }

Attribute

  • Attribute sind Anweisungen an den Compiler, die die Code-Erstellung beeinflussen
  • #[] vor einem Element oder #![] für das aktuelle Element; fn …() { #![…] } äquivalent #[…] fn …() {}; für Module ist Dokumentation sinnvoller innerhalb der (Modul-)Datei, Anweisung für gesamtes Programm in main.rs mit #!
  • typische Attribute, Rust-Buch:
    • [#[derive(Default,Debug,Clone,PartialEq)]]](https://doc.rust-lang.org/reference/attributes/derive.html): der Compiler erzeugt eine Standardimplementierung für den Datentyp, z. B. Clone, indem jedes Element geclont wird (sofern möglich), oder PartialEq, indem alle Elemente verglichen werden (sofern möglich)
    • #[deprecated(since = "5.2", note = "…")]: Kennzeichnung veralteter Funktionen/Datentypen, bei deren Benutzung der Compiler eine Warnung ausgibt
    • #[must_use = "…"]: der Compiler warnt über Objekte (z. B. Rückgabewerte), die nicht genutzt werden; wichtig z. B. bei Result oder Iteratoren
    • #[allow(unused_variables)], #[warn(unused_variables)]: Compiler-Warnungen an- und abschalten
    • #[macro_use] um alle Makros eines Pakets zu importieren; geht mittlerweile schöner mit use …::macro; (ohne Ausrufezeichen)
    • #[cfg(…)] bedingte Kompilierung des Elements in der Cargo-Konfiguration 
  • eigene Attribute definierbar
  • fast jede Zeile kann ein Attribut haben, aber nicht alle Attribute sind immer möglich/sinnvoll

Datentypen definieren

  • type neu = alt
  • Enum
  • Struct:
    • mit benannten Attributen { name: type, … }, Zugriff mit obj.name
    • unbenannte Attribute (type, …), Zugriff mit obj.0
  • impl Type { fn …() { … } }: Assoziierte Funktionen Mod::Type::fn()

Initialisierung

  • es gibt keine speziellen Konstruktoren von Objekten, jede Funktion, die alle Attribute eines Objekts beschreiben kann, kann dafür den Speicher reservieren und das Objekt initialisieren.
  • Initialisierung mit Type { attr1: val1, … } bzw. Type(val1, …)
  • zur Vereinfachung kann man auch überall Self statt des Typs verwenden, erleichtert Vorlagen des Editors
  • daher Objekterzeugung mal mit new(), with_capacity(), from()

Methoden

  • Methoden sind Funktionen eines Objekts; obj.fn()
  • Unterschied: erster Parameter ohne Datentyp ist irgendwas mit self, self, &mut self, Box<self>, Rc<self>; self entspricht this in anderen Sprachen
  • meist verwendet man &self für Funktionen, die nicht das Objekt verändern, und &mut self für Funktionen, die das Objekt verändern, self konsumiert das Objekt
  • this in anderen Sprachen irgendwie magisch, weil nirgendwo definiert; Python macht es explizit als ersten Parameter, tatsächliche Assembler-Implementation ist auch: obj.fn()fn(obj)
  • die Methodenaufrufe sind »nur« eine Erleichterung des Compilers (Syntactic sugar), um nicht jedesmal Typ::fn(obj) schreiben zu müssen und wissen zu müssen, welcher Typ das ist – Überarbeitung des Codes wäre sehr aufwendig
  • aber Rust macht's auch möglich, Methoden als assoziierte Funktion aufzurufen: println!("Results: {} und {}", x.len(), Vec::len(&x))
  • es gibt ein paar magische Methoden:
    • from()/into(): impl From<Other> for Thisimpl Into<This> for Other man implementiert nur From und der Compiler erzeugt Into
  • innerhalb eines Moduls mehrere impl für einen Datentyp möglich

Trait implementieren

TODO

Tests

  • Tests müssen mit dem Attribut #[test] gekennzeichnet werden
  • es gibt noch die Attribute #[ignore] zum Deaktivieren eines Tests und #[should_panic], wenn ein Test mit einer Panic abbrechen soll
  • innerhalb von Tests mit den Makros assert!(…), assert_eq!(…, …) und assert_ne!(…, …) arbeiten, im Fehlerfall lösen diese eine Panic aus und brechen die Methode damit ab
  • cargo test -q
  • cargo test NAME Auswahl einiger Test anhand eines Teils (Substrings) des Namens

TODO: sudoku.rs