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.
Aufzeichnung
Vom ersten Vortrag gibt es leider keine Aufzeichnung, aber beim zweiten Vortrag habe ich bei mir lokal aufzeichnen können:
Code-Struktur
- Hauptdatei bei Programmen (
cargo new --bin
) immersrc/main.rs
, bei Bibliotheken (cargo new --lib
) immersrc/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.
- Nur mit
- 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 Dateixy/yz.rs
xy.rs
undxy/mod.rs
sind äquivalent, ich bevorzugexy/mod.rs
- Datei muss
- 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 beiuse std::io::{self, Read}
- auch Zustände aus Enums können importiert werden
- aus Importen kann wieder importiert werden
- Bibliotheken und Module definieren solche Bereiche
- 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!()
oderprintln!()
;ln
fügt Zeilenumbruch am Ende hinzu - auf stderr mit
eprint!()
undeprintln!()
- 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
(zuvorcargo install cargo-expand
) ansehen
- auf stdout mit
- formatierte Ausgabe in
String
mitformat!("…", …)
- 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 Komponentenenum
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 einerstruct { zustand; union { … } }
- Beispiele:
Option { Some(val), None }
undResult { Ok(val), Err(err) }
- in C hat man Magic-Values wie
NULL
oder-1
missbraucht, um zwischenSome
undNone
zu unterscheiden - Fehler werden in C über
errno
weitergegeben, was für Parallelität eine Katastrophe ist - Zustandabfrage mit Pattern matching:
match var { Zustand => { … }, _ => () }
oderif 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 mituse …::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 mitobj.name
- unbenannte Attribute
(type, …)
, Zugriff mitobj.0
- mit benannten Attributen
impl Type { fn …() { … } }
: Assoziierte FunktionenMod::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 This
⟺impl Into<This> for Other
man implementiert nur From und der Compiler erzeugt Into
- from()/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!(…, …)
undassert_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