Rust

Rust #

Basics #

fn main() {
	println!("Hello, world!");
	}

Funktionen #

Funktionen werden mit dem fn-Schlüsselwort (ausgeprochen: “fun”) eingeführt. Funktionen-Statements werden mit {} abgegrenzt. Der Deklaration des Rückgabetyps wird -> vorangestellt, der Rückgabewert ist standardmässig die letzte Zeile in der Funktion, die ohne Semikolon abgeschlossen werden muss. Bsp. euklidischer Algorithmus zum Auffinden des grössten gemeinsamen Divisors: fn gcd(mut n: u64, mut m: u64) -> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n }

Skalare Typen #

Integers #

Länge Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 (Standard) u32
64-bit i64 u64
arch isize usize

Bereich:

  • Signed: -(2^n-1^) bis 2^n-1 ^- 1
  • Unsigned 0 bis 2^n^ - 1

isize und usize hängen von der Architketur ab, auf der das Programm läuft. Bei einer 32-bit Architektur ist isize bzw. usize identisch mit 32-bit, bei einer 64-bit Architektur mit 64-bit.

Integer-Literale #

Alle numerischen Literale ausser Byte erlauben Typsuffixe und _ als Trenner.

Literal Beispiel
Dezimal 98_222
Hexadezimal 0xff
Oktal 0o77
Binär 0b1111_0000
Byte (nur u8) b’A'

Fliesskoma-Typen

  • f32
  • f64 (Standard)

Boolean #

  • true
  • false

Character #

char. Bereich: Skalara Unicode-Werte

Zusammengesetzte Typen #

Zusammengesetzte Typen (compound types) können mehrere Werte von anderen Typen zu einem Typ vereinigen. Rust kennt zwei primitive zusammengesetzte Typen: tuples und arrays.

Tuples #

Beispiel: let tup: (i32, f64, u8) = (500, 6.4, 1); Werden auf Stack gespeichert Zugriff auf einzelnen Wert: tup.0; etc. Destructuring: let (x, y, z) = tup;

Arrays #

Beispiel: let a = [1, 2, 3, 4, 5];

  • Im Unterschied zu Tuples müssen Array immer skalare Werte desselben Typs haben.
  • Arrays haben immer eine fixe Länge
  • Werden auf Stack gespeichert

Zugriff auf einzelnen Wert: a[0]; etc.

Collections #

Werden im Unterschied zu primitiven zusammengesetzten Typen auf Heap gespeichert

Strings #

String is the dynamic heap string type, like Vec: use it when you need to own or modify your string data. str is an immutable^1^ sequence of UTF-8 bytes of dynamic length somewhere in memory. Since the size is unknown, one can only handle it behind a pointer. This means that str most commonly^2^ appears as &str: a reference to some UTF-8 data, normally called a “string slice” or just a “slice”. A slice is just a view onto some data, and that data can be anywhere, e.g.

  • in static storage: a string literal "foo" is a &'static str. The data is hardcoded into the executable and loaded into memory when the program runs.

  • inside a heap allocated String: String dereferences to a &str view of the String’s data.

  • on the stack: e.g. the following creates a stack-allocated byte array, and then gets a view of that data as a &str:

    use std::str;

    let x: &[u8] = &[b’a’, b’b’, b’c’]; let stack_str: &str = str::from_utf8(x).unwrap();

In summary, use String if you need owned string data (like passing strings to other tasks, or building them at runtime), and use &str if you only need a view of a string. This is identical to the relationship between a vector Vec<T> and a slice &[T], and is similar to the relationship between by-value T and by-reference &T for general types.

^1^A str is fixed length; you cannot write bytes beyond the end, or leave trailing invalid bytes. Since UTF-8 is a variable width encoding, this effectively forces all strs to be immutable. In general, mutation requires writing more or fewer bytes than there were before (e.g. replacing an a (1 byte) with an ä (2+ bytes) would require making more room in the str). ^2^At the moment it can only appear as &str, but dynamically sized types may allow things like Rc for a sequence of reference counted UTF-8 bytes. It also may not, str doesn’t quite fit into the DST scheme perfectly, since there is no fixed size version (yet).

Konzepte #

Ownership #

Etabliert eine definierte Lebenszeit für jeden Wert, der GC im Sprachkern unnötig macht, und Schnittstellen für andere Arten von Ressourcen wie Sockets und File handlers bietet.

Stack #

  • LIFO-Prinzip
  • Durch dieses Prinzip sind Operationen, die auf den Stack zugreifen, schnell, da neue Daten immer nur zuoberst auf dem *stack *abgelegt werden und umgekehrt Daten nur immer von dort geholt werden
  • Eine andere Eigenschaft, die den stack sehr schnell macht, ist, dass nur Daten in ihm gespeichert werden können, deren (fixe) Grösse zur Kompilierzeit bekannt ist. D.h. skalare Datentypen (integers, floats, chars, bools) und primitive *compound types *(arrays, tuples).
  • Pushing onto the stack
  • Popping off the stack

Heap #

  • Pointer (im stack) zeigen auf Daten im heap
  • Allocating (on the heap)

Ownership-Regeln #

  1. Jeder Wert in Rust hat eine Variable, die ihr *owner *genannt wird
  2. Es kann nur immer ein owner zu einer Zeit existieren
  3. Wenn der owner nicht mehr im *scope *ist, dann wird der Wert im Speicher gelöscht

Moves #

Transferieren Werte von einem Owner zu einem anderen

Borrows #

Lassen Code einen Wert benutzen, ohne dass die Ownership angetastet wird.

Smart Pointers #

  • Box
  • Cell
  • RefCell
  • Rc
  • Arc

Structs #

Es existieren drei Typen von Structs:

  • Named-field struct:

    struct GrayscaleMap { pixels: Vec, size: (usize, usize), }

  • Tuple-like struct:

    struct Bounds(usize, usize);

  • Unit-like struct:

    struct Onesuch;

Enums #

Es existieren drei Typen von Enums:

  • “Einfache” Enums:

    enum HttpStatus { Ok, NotModified, NotFound, } Solche Enums werden intern als Integers gespeichert. Optional kann den Elementen auch ein spezfischer Integer-Wert zugeordnet werden (bspw. Ok = 200).

  • Enums mit tuple variants:

    enum RoughTime { InThePast(TimeUnit, u32), InTheFuture(TimeUnit, u32), } Solche Enums werden anschliessend folgendermassen initialisiert: RoughTime::InThePast(TimeUnit::Years, 4*20 + 7);

  • Enums mit struct variants:

    enum Shape { Sphere { center: Point3d, radius: f32 }, Cuboid { corner1: Point3d, corner2: Point3d }, } Sie werden folgendermassen initialisiert: Shape::Sphere { center: ORIGIN, radius: 1.0 };

Ein Enum kann auch eine Mischung aus verschiedenen Typen enthalten

Traits #

Fn Traits #

Trait Object #

The difference between a trait object and a bounded generic is that the latter is always monomorphized during compilation, while the former is an instance of a dynamically sized type and can therefore only be used behind some type of pointer. This pointer actually contains to pointers: One to the data, and one to a vtable (virtual table), which contains in its turn, for each method of the trait and its supertraits that T implements, a pointer to T’s implementation (i.e. a function pointer).

Important Traits #

  1. Important Traits in the Standard Libary

Types #

Dynamically Sized Types (DST) #

Newtype Pattern #

Module #

Begrifflichkeiten #

  • Packages sind ein Feature von cargo, das es erlaubt, crates zu erstellen, testen und publizieren.
  • Crates sind ein Modulbaum in Form einer Binary oder einer Library
  • Mit Modules und dem use Schlüsselwort lassen sich die Reichweite und die Öffentlichkeit von paths steuern
  • Paths ist ein Weg, ein item wie ein struct, eine Funktion oder ein Modul zu benennen

Packages #

  • Cargo.toml + src/main.rs: Cargo erstellt eine Binary mit dem gleichen Namen wie das Package, und main.rs ist die crate root
  • Cargo.toml + src/lib.rs: Cargo erstellt eine Library mit dem gleichen Namen wie das Package, und lib.rs ist die crate root
  • Befinden sich sowohl main.rs als auch lib.rs im src/-Verzeichnis, werden zwei Crates erstellt: Eine Library und eine Binary
  • Ein Package kann 0-1 Library Crates enthalten und beliebig viele Binary Crates
  • Soll mehr als ein Binary Crate erstellt werden, werden die entsprechenden Dateien in src/bin/ abgelegt

Module #

  • Module helfen, Code zu gruppieren
  • Ein Modul wird mit dem mod-Schlüsselwort erstellt. Sein Inhalt befindet sich in geschweiften Klammern
  • Die Crate Root befindet sich implizit im Module crate

Pfade #

  • Absoluter Pfad: Beginnt mit dem Name der Crate oder mit einem literalen crate
  • Relativer Pfad: Startet vom aktuellen Module und hat als erstes Element self, super oder ein Identifier im aktuellen Modul

Sichtbarkeitsregeln #

  • Alle Elemente (Funktionen, Methoden, structs, enums, Modules und Konstanten) sind standardmässig privat
  • Ist ein Element privat, können nur Elemente in demselben Modul und dessen Kindmodulen darauf zugreifen.
  • Mit dem Schlüsselwort pub wird ein Element öfffentlich und ist von überallher zugänglich

Overall, these are the rules for item visibility:

If an item is public, it can be accessed through any of its parent modules. If an item is private, it can be accessed only by its immediate parent module and any of the parent’s child modules.

Advanced Error Handling #

Benötigt:

  1. Ein Enum mit den benötigten Fehlerklassen (implementiert als tuple-like variants)

  2. Enum implementiert Traits Debug (derivable) und Display (nicht derivable)

  3. Enum implementiert From-Traits für die benötigten Typkonversionen (fremder Error-Typ -> eigener Error-Typ)

  4. Optional: type-Definition für Result-Alias

  5. auch die Crate error_chain2

Beispiel: use std::error; use std::fmt::{self, Display, Formatter}; use std::io; use std::num::ParseIntError; use std::result;

#[derive(Debug)]
enum Error {
	Io(io::Error),
	Parse(ParseIntError),
}

// The trait Display is used for user-facing output (other than the Debug trait)
impl Display for Error {
	fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
		match *self {
			Error::Io(ref error) => error.fmt(formatter),
			Error::Parse(ref error) => error.fmt(formatter),
		}
	}
}

// Error is a trait representing the basic expectations for error values,
// i.e. values of type E in Result<T, E>. Errors must describe themselves
// through the Display and Debug traits
impl error::Error for Error {
}

// Needed for implicit casting
impl From<io::Error> for Error {
	fn from(error: io::Error) -> Self {
		Error::Io(error)
	}
}

impl From<ParseIntError> for Error {
	fn from(error: ParseIntError) -> Self {
		Error::Parse(error)
	}
}

type Result<T> = result::Result<T, Error>;

fn main() -> Result<()> {
	let mut line = String::new();
	std::io::stdin().read_line(&mut line)?;
	let mut sum = 0;
    for word in line.split_whitespace() {
        let num = word.parse::<i64>()?;
        sum += num;
    }
    println!("Sum: {}", sum);
    Ok(())
}

Weitere Ressourcen #