Rust ist eine Pro­gram­mier­spra­che von Mozilla. Mit Rust lassen sich u. a. Kom­man­do­zei­len-Tools, Web-Ap­pli­ka­tio­nen und Netz­werk­pro­gram­me schreiben. Ferner eignet sich die Sprache für hard­ware­na­he Pro­gram­mie­rung. Unter Rust-Pro­gram­mie­rern erfreut sich die Sprache aus­ge­spro­che­ner Be­liebt­heit.

In diesem Rust-Tutorial zeigen wir Ihnen die wich­tigs­ten Features der Sprache. Dabei gehen wir auf Ge­mein­sam­kei­ten und Un­ter­schie­de zu anderen, ver­brei­te­ten Sprachen ein. Wir leiten Sie durch die Rust-In­stal­la­ti­on, und Sie lernen, Rust-Code auf Ihrem eigenen System zu schreiben und zu kom­pi­lie­ren.

Die Pro­gram­mier­spra­che Rust im Überblick

Bei Rust handelt es sich um eine kom­pi­lier­te Sprache. Diese Ei­gen­schaft re­sul­tiert in hoher Per­for­manz; gleich­zei­tig bietet die Sprache aus­ge­reif­te Abs­trak­tio­nen, die dem Pro­gram­mie­rer die Arbeit er­leich­tern. Ein be­son­de­rer Fokus von Rust liegt auf der Spei­cher­si­cher­heit. Damit hat die Sprache einen be­son­de­ren Vorteil im Vergleich zu älteren Sprachen wie C und C++.

Rust auf dem eigenen System nutzen

Da Rust eine freie und quell­of­fe­ne Software (FOSS) ist, kann ein jeder die Rust-Toolchain her­un­ter­la­den und auf dem eigenen System nutzen. Anders als Python oder Ja­va­Script ist Rust keine in­ter­pre­tier­te Sprache. Statt einem In­ter­pre­ter kommt, wie bei C, C++ und Java, ein Compiler zum Einsatz. In der Praxis bedeutet dies, dass zum Ausführen von Code zwei Schritte gehören:

  1. Den Quelltext kom­pi­lie­ren. Dies erzeugt eine aus­führ­ba­re Bi­när­da­tei.
  2. Die re­sul­tie­ren­de Bi­när­da­tei ausführen.

Beide Schritte werden im ein­fachs­ten Fall über die Kom­man­do­zei­le gesteuert.

Tipp

In einem anderen Digital-Guide-Artikel schauen wir uns den Un­ter­schied zwischen Compiler und In­ter­pre­ter genauer an.

Mit Rust können neben aus­führ­ba­ren Bi­när­da­tei­en auch Bi­blio­the­ken erzeugt werden. Handelt es sich beim kom­pi­lier­ten Code um ein direkt aus­führ­ba­res Programm, muss im Quelltext eine main()-Funktion definiert werden. Wie in C / C++ dient diese als Ein­stiegs­punkt in die Code-Aus­füh­rung.

Rust für das Tutorial auf dem lokalen System in­stal­lie­ren

Um Rust nutzen zu können, benötigen Sie zunächst eine lokale In­stal­la­ti­on. Unter macOS nutzen Sie dabei den Homebrew Pa­ket­ma­na­ger. Homebrew funk­tio­niert auch unter Linux. Öffnen Sie eine Kom­man­do­zei­le (‚Terminal.App‘ auf dem Mac), kopieren Sie die folgende Codezeile in das Terminal und führen Sie diese aus:

brew install rust
Hinweis

Um Rust auf Windows oder einem anderen System ohne Homebrew zu in­stal­lie­ren, nutzen Sie das of­fi­zi­el­le Tool Rustup.

Um zu über­prü­fen, ob die Rust-In­stal­la­ti­on er­folg­reich war, öffnen Sie ein neues Fenster auf der Kom­man­do­zei­le und führen Sie den folgenden Code aus:

rustc --version

Sofern Rust korrekt auf Ihrem System in­stal­liert wurde, wird Ihnen die Version des Rust-Compilers angezeigt. Sollte statt­des­sen eine Feh­ler­mel­dung er­schei­nen, starten Sie die In­stal­la­ti­on ggf. erneut.

Rust-Code kom­pi­lie­ren

Zum Kom­pi­lie­ren von Rust-Code benötigen Sie eine Rust-Quell­text­da­tei. Öffnen Sie die Kom­man­do­zei­le und führen Sie die nach­fol­gen­den Code-Stücke aus. Wir werden zunächst einen Ordner für das Rust-Tutorial auf dem Desktop anlegen und in den Ordner wechseln:

cd "$HOME/Desktop/"
mkdir rust-tutorial && cd rust-tutorial

Als nächstes erzeugen wir die Rust Quelltext-Datei für ein simples „Hello, World“-Beispiel:

cat << EOF > ./rust-tutorial.rs
fn main() {
    println!("Hello, World!");
}
EOF
Hinweis

Rust-Quell­text­da­tei­en enden mit dem Kürzel .rs.

Ab­schlie­ßend werden wir den Rust-Quelltext kom­pi­lie­ren und die re­sul­tie­ren­de Bi­när­da­tei ausführen:

# Rust-Quelltext kompilieren
rustc rust-tutorial.rs
# resultierende Binärdatei ausführen
./rust-tutorial
Tipp

Nutzen Sie in den Befehl rustc rust-tutorial.rs && ./rust-tutorial, um die beiden Schritte zu kom­bi­nie­ren. So können Sie auf der Kom­man­do­zei­le durch Drücken der Pfeil­tas­te nach oben gefolgt von Enter Ihr Programm erneut kom­pi­lie­ren und ausführen.

Rust-Pakete mit Cargo verwalten

Neben der ei­gent­li­chen Rust-Sprache gibt es eine Vielzahl externer Pakete. Diese so­ge­nann­ten Crates lassen sich von der Rust Package Registry beziehen. Dazu wird das zusammen mit Rust in­stal­lier­te Tool Cargo verwendet. Der cargo-Befehl wird auf der Kom­man­do­zei­le genutzt und erlaubt neben der In­stal­la­ti­on von Paketen auch das Erstellen neuer Pakete. Über­prü­fen Sie, ob Cargo korrekt in­stal­liert wurde:

cargo --version

Rust-Grund­la­gen lernen

Um Rust zu lernen, empfehlen wir Ihnen, die Code-Beispiele selbst aus­zu­pro­bie­ren. Sie können dafür die bereits angelegte Datei rust-tutorial.rs nutzen. Kopieren Sie ein Code-Beispiel in die Datei, kom­pi­lie­ren Sie diese und führen Sie die re­sul­tie­ren­de Bi­när­da­tei aus. Damit dies funk­tio­niert, muss der Beispiel-Code innerhalb der main()-Funktion eingefügt werden!

Sie können auch den Rust Play­ground direkt in Ihrem Browser nutzen, um Rust-Code aus­zu­pro­bie­ren.

An­wei­sun­gen und Blöcke

An­wei­sun­gen sind grund­le­gen­der Code-Baustein in Rust. Eine Anweisung endet mit einem Semikolon (;) und gibt, anders als ein Ausdruck, keinen Wert zurück. Mehrere An­wei­sun­gen können in einem Block gruppiert werden. Blöcke werden wie in C/C++ und Java von ge­schweif­ten Klammern '{}' begrenzt.

Kom­men­ta­re in Rust

Kom­men­ta­re sind ein wichtiges Feature einer jeden Pro­gram­mier­spra­che. Sie dienen sowohl zum Do­ku­men­tie­ren des Codes als auch zur Planung, bevor der ei­gent­lich Code ge­schrie­ben wird. Rust verwendet dieselbe Kommentar-Syntax wie C, C++, Java und Ja­va­Script: Jeglicher Text nach einem doppelten Schräg­strich wird als Kommentar in­ter­pre­tiert und vom Compiler ignoriert:

// Hier steht ein Kommentar
// Ein Kommentar,
// der sich über
// mehrere Zeilen
// erstreckt.

Variablen und Kon­stan­ten

Wir nutzen in Rust das Schlüs­sel­wort ‚let‘, um eine Variable zu de­kla­rie­ren. Eine be­stehen­de Variable kann in Rust erneut de­kla­riert werden und „über­schat­tet“ dann die be­stehen­de Variable. Im Un­ter­schied zu vielen anderen Sprachen kann der Wert einer Variable nicht ohne weiteres geändert werden:

// Variable ‚alter‘ deklarieren und Wert auf ‚42‘ setzen
let alter = 42;
// Wert der Variable ‚alter‘ kann nicht verändert werden
alter = 49; // Kompilier-Fehler
// mit erneutem ‚let‘ kann die Variable überschrieben werden
let alter = 49;

Um den Wert einer Variable als im Nach­hin­ein änderbar zu markieren, bringt Rust das ‚mut‘-Schlüs­sel­wort mit. Der Wert einer mit ‚mut‘ de­kla­rier­ten Variablen lässt sich verändern:

let mut gewicht = 78;
gewicht = 75;

Mit dem Schlüs­sel­wort ‚const‘ wird eine Konstante erzeugt. Der Wert einer Rust-Konstante muss bei der Kom­pi­lie­rung bekannt sein. Ferner muss der Typ explizit angegeben werden:

const VERSION: &str = "1.46.0";

Der Wert einer Konstante kann nicht verändert werden – eine Konstante lässt sich auch nicht als ‚mut‘ de­kla­rie­ren. Ferner lässt sich eine Konstante nicht neu de­kla­rie­ren:

// Konstante definieren
const MAX_NUM: u8 = 255;
MAX_NUM = 0; // Kompilier-Fehler, da der Wert einer Konstante nicht veränderbar ist
const MAX_NUM = 0; // Kompilier-Fehler, da Konstante nicht neu deklariert werden kann

Konzept des Be­sitz­tums in Rust

Eines der aus­schlag­ge­ben­den Features von Rust ist das Konzept des Be­sitz­tums (engl. „Ownership“). Das Besitztum ist eng verknüpft mit dem Wert von Variablen, deren „Lifetime“ und der Spei­cher­ver­wal­tung von Objekten auf dem „Heap“-Speicher. Wenn eine Variable den Gül­tig­keits­be­reich („Scope“) verlässt, wird ihr Wert zerstört und der Speicher frei­ge­ge­ben. Rust kommt daher ohne „Garbage Coll­ec­tion“ aus, was zur hohen Per­for­manz beiträgt.

Jeder Wert in Rust gehört einer Variablen – dem Besitzer. Es kann für jeden Wert nur einen Besitzer geben. Wenn der Besitzer den Wert wei­ter­reicht, dann ist er nicht mehr Besitzer:

let name = String::from("Peter Mustermann");
let _name = name;
println!("{}, world!", name); // Kompilier-Fehler, da der Wert von ‚name‘ an ‚_name‘ weitergereicht wurde

Besondere Vorsicht gilt beim De­fi­nie­ren von Funk­tio­nen: Wird eine Variable an eine Funktion übergeben, ändert sich der Besitzer des Werts. Die Variable kann nach dem Funk­ti­ons­auf­ruf nicht wei­ter­ver­wen­det werden. Hier bedient man sich in Rust eines Tricks: Anstatt den Wert selbst an die Funktion zu übergeben, wird mit dem Ampersand-Symbol (&) eine Referenz de­kla­riert. Damit lässt sich der Wert einer Variable „borgen“. Hier ein Beispiel:

let name = String::from("Peter Mustermann");
// wird der Typ des ‚name‘-Parameters als ‚String‘ statt ‚&String‘ definiert
// kann die Variable ‚name‘ nach dem Funktionsaufruf nicht mehr verwendet werden
fn hallo(name: &String) {
    println!("Hallo, {}", name);
}
// das Funktions-Argument muss ebenfalls mit ‚&‘
// als Referenz gekennzeichnet werden
hallo(&name);
// diese Zeile führt ohne Nutzung der Referenz zum Kompilier-Fehler
println!("Hallo, {}", name);

Kon­troll­struk­tu­ren

Eine grund­le­gen­de Ei­gen­schaft der Pro­gram­mie­rung besteht darin, den Pro­gramm­ab­lauf nicht­li­ne­ar zu gestalten. Ein Programm kann ver­zwei­gen, ferner können Pro­gramm­be­stand­tei­le mehrfach aus­ge­führt werden. Erst durch diese Va­ria­bi­li­tät wird ein Programm wirklich nutzbar.

Rust verfügt über die in den meisten Pro­gram­mier­spra­chen vor­han­de­nen Kon­troll­struk­tu­ren. Dazu gehören die Schleifen-Kon­struk­te ‚for‘ und ‚while‘, sowie die Ver­zwei­gun­gen via ‚if‘ und ‚else‘. Daneben bringt Rust auch einige Be­son­der­hei­ten mit. So erlaubt das ‚match‘-Konstrukt die Zuordnung von Mustern, während die ‚loop‘-Anweisung eine End­los­schlei­fe erzeugt. Um letzteres prak­ti­ka­bel zu machen, wird eine ‚break‘-Anweisung genutzt.

Schleifen

Die wie­der­hol­te Aus­füh­rung eines Code-Blocks mittels Schleifen ist auch als „Iteration“ bekannt. Oft wird über den Elementen eines Con­tai­ners iteriert. Wie Python kennt Rust das Konzept des „Iterators“. Ein Iterator abs­tra­hiert den suk­zes­si­ven Zugriff auf die Elemente eines Con­tai­ners. Schauen wir uns ein Beispiel an:

// Liste mit Namen
let namen = ["Jim", "Jack", "John"];
// ‚for‘-Schleife mit Iterator in die Liste
for name in namen.iter() {
    println!("Hallo, {}", name);
}

Wie verhält es sich nun, wenn Sie eine ‚for‘-Schleife im Stil von C/C++ oder Java schreiben möchten? Sie möchten also eine An­fangs­zahl und Endzahl festlegen und alle Werte da­zwi­schen durch­lau­fen. Für diesen Fall gibt es in Rust, wie auch in Python, das „Range“-Objekt. Dieses erzeugt wiederum einen Iterator, auf dem das ‚for‘-Schlüs­sel­wort operiert:

// die Zahlen ‚1‘ bis ‚10‘ ausgeben
// ‚for‘-Schleife mit ‚range‘-Iterator
// Achtung: Die Range enthält nicht die Endzahl!
for zahl in 1..11 {
    println!("Zahl: {}", zahl);
}
// alternative (inklusive) Range-Schreibweise
for zahl in 1..=10 {
    println!("Zahl: {}", zahl);
}

Eine ‚while‘-Schleife funk­tio­niert in Rust genauso wie in den meisten anderen Sprachen auch. Es wird eine Kondition fest­ge­legt und der Schlei­fen­kör­per so lange aus­ge­führt, wie die Kondition wahr ist:

// die Zahlen ‚1‘ bis ‚10‘ via ‚while‘-Schleife ausgeben
let mut zahl = 1;
while (zahl <= 10) {
    println!(Zahl: {}, zahl);
    zahl += 1;
}

Es ist allen Pro­gram­mier­spra­chen möglich, mit ‚while‘ eine End­los­schlei­fe zu erzeugen. Nor­ma­ler­wei­se handelt es sich dabei um einen Fehler, es gibt aber auch An­wen­dungs­fäl­le, die dies erfordern. Rust kennt für solche Fälle die ‚loop‘-Anweisung:

// Endlosschleife mit ‚while‘
while true {
    // …
}
// Endlosschleife mit ‚loop‘
loop {
    // …
}

In beiden Fällen kann das ‚break‘-Schlüs­sel­wort verwendet werden, um aus der Schleife aus­zu­bre­chen.

Ver­zwei­gun­gen

Auch die Ver­zwei­gung mit ‚if‘ und ‚else‘ funk­tio­niert in Rust genauso wie in ver­gleich­ba­ren Sprachen:

const limit: u8 = 42;
let zahl = 43;
if zahl < limit {
    println!("Unter dem Limit.");
}
else if zahl == limit {
    println!("Genau am Limit…");
}
else {
    println!("Über dem Limit!");
}

In­ter­es­san­ter ist Rusts ‚match‘-Schlüs­sel­wort. Dieses erfüllt eine ähnliche Funktion wie die ‚switch‘-Anweisung anderer Sprachen. Schauen Sie sich für ein Beispiel die Funktion karten_symbol() im Abschnitt „Zu­sam­men­ge­setz­te Da­ten­ty­pen“ (s. u.) an.

Funk­tio­nen, Pro­ze­du­ren und Methoden

In den meisten Pro­gram­mier­spra­chen sind Funk­tio­nen grund­le­gen­der Baustein modularer Pro­gram­mie­rung. Funk­tio­nen werden in Rust mit dem Schlüs­sel­wort ‚fn‘ definiert. Dabei wird nicht strikt zwischen den ver­wand­ten Konzepten Funktion und Prozedur un­ter­schie­den. Beide werden auf nahezu iden­ti­sche Weise definiert.

Eine Funktion im ei­gent­li­chen Sinn liefert einen Wert zurück. Wie viele andere Pro­gram­mier­spra­chen kennt Rust auch Pro­ze­du­ren, d. h. Funk­tio­nen, die keinen Wert zu­rück­lie­fern. Als einzige feste Ein­schrän­kung gilt, dass der Rück­ga­be­typ einer Funktion explizit angegeben werden muss. Wird kein Rück­ga­be­typ angegeben, kann die Funktion auch keinen Wert zu­rück­ge­ben; dann handelt es sich der De­fi­ni­ti­on nach um eine Prozedur.

fn prozedur() {
    println!("Diese Prozedur liefert keinen Wert zurück.");
}
// eine Zahl negieren
// Rückgabetyp nach dem ‚->‘-Operator
fn negiert(ganzzahl: i8) -> i8 {
    return ganzzahl * -1;
}

Neben Funk­tio­nen und Pro­ze­du­ren kennt Rust auch die aus der ob­jekt­ori­en­tier­ten Pro­gram­mie­rung bekannten Methoden. Eine Methode ist eine Funktion, die an eine Da­ten­struk­tur gebunden ist. Wie in Python werden Methoden in Rust mit dem ersten Parameter ‚self‘ definiert. Der Aufruf einer Methode erfolgt nach dem üblichen Schema objekt.methode(). Hier ein Beispiel der Methode flaeche(), gebunden an eine ‚struct‘-Da­ten­struk­tur:

// ‚struct‘-Definition
struct Rechteck {
    breite: u32,
    hoehe: u32,
}
// ‚struct‘-Implementierung
impl Rechteck {
    fn flaeche(&self) -> u32 {
        return self.breite * self.hoehe;
    }
}
let rechteck = Rechteck {
    breite: 30,
    hoehe: 50,
};
println!("Die Fläche des Rechtecks beträgt {}.", rechteck.flaeche());

Da­ten­ty­pen und Da­ten­struk­tu­ren

Bei Rust handelt es sich um eine statisch ty­pi­sier­te Sprache. Anders als in den dynamisch ty­pi­sier­ten Sprachen Python, Ruby, PHP oder Ja­va­Script muss in Rust der Typ einer jeden Variable bei der Kom­pi­lie­rung bekannt sein.

Ele­men­ta­re Da­ten­ty­pen

Wie die meisten höheren Pro­gram­mier­spra­chen kennt Rust einige ele­men­ta­re Da­ten­ty­pen („Pri­mi­ti­ves“). Instanzen ele­men­ta­rer Da­ten­ty­pen werden auf dem Stack-Speicher zu­ge­wie­sen, was besonders per­for­mant ist. Ferner lassen sich die Werte ele­men­ta­rer Da­ten­ty­pen per „Literal“-Syntax de­fi­nie­ren. Das bedeutet, die Werte können einfach aus­ge­schrie­ben werden.

Datentyp Erklärung Typ-An­no­ta­ti­on
Integer ganze Zahl i8, u8, etc.
Floating point Gleit­kom­ma­zahl f64, f32
Boolean Wahr­heits­wert bool
Character einzelner Unicode-Buchstabe char
String Unicode-Zei­chen­ket­te str

Obwohl Rust eine statisch ty­pi­sier­te Sprache ist, muss der Typ eines Werts nicht immer explizit de­kla­riert werden. In vielen Fällen kann der Typ vom Compiler aus dem Kontext ab­ge­lei­tet werden („Type inference“). Al­ter­na­tiv wird der Typ explizit per Typ-An­no­ta­ti­on angegeben. In einigen Fällen ist Letzteres zwingend notwendig:

  • Der Rück­ga­be­typ einer Funktion muss immer explizit angegeben werden.
  • Der Typ einer Konstante muss immer explizit angegeben werden.
  • String-Literale müssen speziell behandelt werden, damit deren Größe bei Kom­pi­lie­rung bekannt ist.

Hier ein paar an­schau­li­che Beispiele für die In­stan­zi­ie­rung ele­men­ta­rer Da­ten­ty­pen mit Literal-Syntax:

// hier erkennt der Compiler den Typ der Variable automatisch
let cents = 42;
// Typ-Annotation: positive Zahl (‚u8‘ = "unsigned, 8 bits")
let alter: u8 = -15; // Kompilier-Fehler, da negativer Wert gegeben
// Gleitkommazahl
let winkel = 38.5;
// equivalent zu
let winkel: f64 = 38.5;
// Wahrheitswert
let nutzer_angemeldet = true;
// equivalent zu
let nutzer_angemeldet: bool = true;
// Buchstabe, benötigt einzelne Anführungszeichen
let buchstabe = 'a';
// statische Zeichenkette, benötigt doppelte Anführungszeichen
let name = "Walther";
// mit explizitem Typ
let name: &'static str = "Walther";
// alternativ als dynamischer ‚String‘ mit ‚String::from()‘
let name: String = String::from("Walther");

Zu­sam­men­ge­setz­te Da­ten­ty­pen

Ele­men­ta­re Datenypen bilden einzelne Werte ab, wo­hin­ge­gen zu­sam­men­ge­setz­te Da­ten­ty­pen mehrere Werte bündeln. Rust stellt dem Pro­gram­mie­rer eine Handvoll zu­sam­men­ge­setz­ter Da­ten­ty­pen zur Verfügung.

Die Instanzen zu­sam­men­ge­setz­ter Da­ten­ty­pen werden, wie Instanzen ele­men­ta­rer Da­ten­ty­pen, auf dem Stack zu­ge­wie­sen. Um dies zu er­mög­li­chen, müssen die Instanzen eine feste Größe haben. Daraus folgt auch, dass sie sich nach der In­stan­zi­ie­rung nicht beliebig verändern lassen. Hier eine Übersicht der wich­tigs­ten zu­sam­men­ge­setz­ten Da­ten­ty­pen in Rust:

Datentyp Erklärung Typ der Elemente Literal-Syntax
Array Liste mehrerer Werte selber Typ [a1, a2, a3]
Tuple Anordnung mehrerer Werte jeglicher Typ (t1, t2)
Struct Grup­pie­rung mehrerer benannter Werte jeglicher Typ
Enum Auf­zäh­lung jeglicher Typ

Schauen wir uns zunächst eine ‚struct‘-Da­ten­struk­tur an. Wir de­fi­nie­ren eine Person mit drei benannten Feldern:

struct Person = {
    vorname: String,
    nachname: String,
    alter: u8,
}

Um eine konkrete Person ab­zu­bil­den, in­stan­zi­ie­ren wir die ‚struct‘:

let spieler = Person {
    vorname: String::from("Peter"),
    nachname: String::from("Mustermann"),
    alter: 42,
};
// auf Feld einer ‚struct‘-Instanz zugreifen
println!("Alter des Spielers ist: {}", spieler.alter);

Ein ‚enum‘ (kurz für „Enu­me­ra­ti­on”, zu Deutsch „Auf­zäh­lung”) bildet mögliche Varianten einer Ei­gen­schaft ab. Wir stellen dies hier am simplen Beispiel der vier möglichen Farben einer Spiel­kar­te dar:

enum KartenFarbe {
    Kreuz,
    Pik,
    Herz,
    Karo,
}
// die Farbe einer konkreten Spielkarte
let farbe = KartenFarbe::Kreuz;

Rust kennt das ‚match‘-Schlüs­sel­wort für „Pattern matching“, zu Deutsch „Muster-Zuordnung“. Die Funk­tio­na­li­tät ist ver­gleich­bar mit der ‚switch‘-Anweisung anderer Sprachen. Hier ein Beispiel:

// das zu einer Kartenfarbe gehörige Symbol ermitteln
fn karten_symbol(farbe: KartenFarbe) -> &'static str {
    match farbe {
        KartenFarbe::Kreuz => "♣︎",
        KartenFarbe::Pik => "♠︎",
        KartenFarbe::Herz => "♥︎",
        KartenFarbe::Karo => "♦︎",
    }
}
println!("Symbol: {}", karten_symbol(KartenFarbe::Kreuz)); // gibt Symbol ♣︎ aus

Ein Tupel ist eine Anordnung mehrerer Werte, die un­ter­schied­li­chen Typs sein können. Die einzelnen Werte des Tupels können durch De­struk­tu­rie­rung mehreren Variablen zu­ge­wie­sen werden. Benötigt man einen der Werte nicht, wird – wie in Haskell, Python und Ja­va­Script üblich – der Un­ter­strich (_) als Platz­hal­ter verwendet. Hier ein Beispiel:

// Spielkarte als Tupel definieren
let spielkarte: (KartenFarbe, u8) = (KartenFarbe::Herz, 7);
// die Werte eines Tupels mehreren Variablen zuweisen
let (farbe, wert) = spielkarte;
// sollten wir nur den Wert benötigen
let (_, wert) = spielkarte;

Da Tupel-Werte geordnet sind, ist auch der Zugriff über einen nu­me­ri­schen Index möglich. Die In­di­zie­rung erfolgt nicht in eckigen Klammern, sondern über eine Punkt-Syntax. In den meisten Fällen dürfte De­struk­tu­rie­rung zu besser lesbarem Code führen:

let name = ("Peter", "Mustermann");
let vorname = name.0;
let nachname = name.1;

Höhere Pro­gram­mier­kon­struk­te in Rust lernen

Dy­na­mi­sche Da­ten­struk­tu­ren

Die bereits vor­ge­stell­ten zu­sam­men­ge­setz­ten Da­ten­ty­pen haben gemeinsam, dass ihre Instanzen auf dem Stack zu­ge­wie­sen werden. Rusts Stan­dard­bi­blio­thek enthält ferner eine Reihe häufig ge­brauch­ter dy­na­mi­scher Da­ten­struk­tu­ren. Instanzen dieser Da­ten­struk­tu­ren werden auf dem Heap zu­ge­wie­sen. Daraus folgt, dass die Größe der Instanzen im Nach­hin­ein verändert werden kann. Hier eine kurze Übersicht häufig genutzter dy­na­mi­scher Da­ten­struk­tu­ren:

Datentyp Erklärung
Vector dy­na­mi­sche Liste mehrerer Werte desselben Typs
String dy­na­mi­sche Folge von Unicode-Buch­sta­ben
HashMap dy­na­mi­sche Zuordnung von Schlüssel-Wert-Paaren

Hier ein Beispiel eines dynamisch wach­sen­den Vektors:

// Vektor mit ‚mut‘ als veränderbar deklarieren
let mut namen = Vec::new();
// dem Vektor Werte hinzufügen
namen.push("Jim");
namen.push("Jack");
namen.push("John");

Ob­jekt­ori­en­tier­te Pro­gram­mie­rung (OOP) in Rust

Im Gegensatz zu Sprachen wie C++ und Java kennt Rust kein Konzept von Klassen. Dennoch lässt sich der OOP-Me­tho­do­lo­gie folgend pro­gram­mie­ren. Grundlage sind die bereits vor­ge­stell­ten Da­ten­ty­pen. Ins­be­son­de­re der ‚struct‘-Typ kann benutzt werden, um die Struktur von Objekten zu de­fi­nie­ren.

Ferner gibt es in Rust die „Traits“, zu Deutsch „Merkmale“. Ein Trait bündelt eine Menge von Methoden, die dann vom einem be­lie­bi­gen Typ im­ple­men­tiert werden können. Dabei umfasst ein Trait Methoden-De­kla­ra­tio­nen, kann jedoch auch Im­ple­men­ta­tio­nen enthalten. Kon­zep­tu­ell liegt ein Trait damit in etwa zwischen einem Java-Interface und einer abs­trak­ten Ba­sis­klas­se.

Ein be­stehen­der Trait kann von ver­schie­de­nen Typen im­ple­men­tiert werden. Überdies kann ein Typ mehrere Traits im­ple­men­tie­ren. Rust erlaubt somit das Kom­po­nie­ren von Funk­tio­na­li­tät für ver­schie­de­ne Typen, ohne dass diese von einem ge­mein­sa­men Vorfahren erben müssen.

Meta-Pro­gram­mie­rung

Wie viele andere Pro­gram­mier­spra­chen erlaubt auch Rust, Code für Meta-Pro­gram­mie­rung zu schreiben. Dabei handelt es sich um Code, der weiteren Code erzeugt. Dazu gehören in Rust zum einen die „Macros“, die Sie viel­leicht so ähnlich aus C/C++ kennen. Macros enden mit einem Aus­ru­fungs­zei­chen (!); das Macro ‚println!‘ zur Ausgabe von Text auf der Kom­man­do­zei­le ist Ihnen in diesem Artikel schon mehrfach un­ter­ge­kom­men.

Zum anderen kennt Rust auch „Generics“. Diese erlauben das Schreiben von Code, der über mehrere Typen abs­tra­hiert. Generics sind in etwa ver­gleich­bar mit den Templates in C++ oder den gleich­na­mi­gen Generics in Java. Ein in Rust häufig genutztes Generic ist ‚Option<T>‘, was für einen be­lie­bi­gen Typ ‚T‘ die Dualität ‚None‘/‚Some(T)‘ abs­tra­hiert.

Fazit

Rust hat das Potenzial, als System-Pro­gram­mier­spra­che die alt­ein­ge­ses­se­nen Favoriten C und C++ zu ersetzen.

Zum Hauptmenü