Ferris Talk #2: Abstraktionen ohne Mehraufwand – Traits in Rust

Die zweite Ausgabe der neuen Rust-Kolumne von Stefan Baumgartner und Rainer Stropek stellt eine Stärke von Rust vor: Traits sind mehr als klassische Interfaces.

In Pocket speichern vorlesen Druckansicht
Ferris Talk – Neuigkeiten zu Rust. Eine Heise-Kolumne von Rainer Stropek und Stefan Baumgartner für Rustaceans
Lesezeit: 12 Min.
Von
  • Stefan Baumgartner
Inhaltsverzeichnis

In unserer ersten Ausgabe haben wir ausgiebig über Iteratoren und ihre Finessen in Rust gesprochen. Dabei haben wir ein kleines Detail vorausgesetzt, das bei Rust-Neueinsteigerinnen und -einsteigern noch Fragen aufwerfen mag: Traits. Dieses fundamentale Sprachkonstrukt wollen wir in der aktuellen Kolumne näher beleuchten.

Ferris Talks – die Kolumne für Rustaceans
Ferris Talk – Neuigkeiten zu Rust. Die neue Kolumne für Rustaceans

Vielfach heißt es, dass sich die Welt im Bereich der Softwareentwicklung schneller ändert als in anderen Bereichen. Auch wenn das stimmen mag, sind fundamentale Umbrüche mit langfristiger Wirkung auch in der Informatik selten. Nicht jedes JavaScript-Framework stellt die Art, Software zu entwickeln, komplett auf den Kopf. Rust aber gehört zu den seltenen Änderungen, die nachhaltig, langfristig und unserer Einschätzung nach positiv wirken werden.

In dieser Kolumne möchten die beiden Rust-Experten Rainer Stropek und Stefan Baumgartner abwechselnd regelmäßig über Neuerungen und Hintergründe im Bereich Rust berichten. Sie soll Teams, die Rust schon verwenden, helfen, auf dem Laufenden zu bleiben. Einsteigerinnen und Einsteiger sollen durch die Kolumne tiefere Einblicke in die Funktionsweise von Rust erhalten.

Kolumnenautoren vom Rust Meetup Linz

Der Kolumnen-Titel nimmt Bezug auf Ferris, das krabbenförmige inoffizielle Maskottchen der Rust-Gemeinde. Die Ferris Talks schreiben Stropek und Baumgartner ab sofort monatlich und im Wechsel – mehr zu den Autoren steht am Ende des Artikels. Die beiden Kolumnisten sind überzeugte Rustaceans und organisieren das Rust Meetup Linz, die Fachtreffen werden in Videoform festgehalten und sind auf der Rust-Linz-Playliste bei YouTube abrufbar. Wer die beiden beruflich treffen möchte, kann sie unter anderem als Vortragende und als Workshop-Trainer bei der Rust-Konferenz 2021 von Heise erleben.

Aaron Turon, einer der langjährigen Rust- und Mozilla-Veteranen, hat vor ein paar Jahren Traits als eine der tragenden Säulen im Sprachdesign von Rust beschrieben. Tatsächlich eröffnen sie eine erstaunliche Flexibilität, die man so eigentlich nur von dynamischen Hochsprachen gewohnt ist. Dabei folgen sie immer dem Rust-Mantra, dass Flexibilität keine Laufzeit kostet.

Grundsätzlich sind die Traits Rusts Version von Interfaces, die man auch aus anderen Programmiersprachen kennt. Sie bieten eine gemeinsame Beschreibung, mit der sich das Verhalten über Typen hinweg definieren lässt. Auch in der grundlegenden Anwendung unterscheiden sie sich kaum von den klassischen Schnittstellenbeschreibungen.

Das folgende Beispiel zeigt zur Veranschaulichung zwei eigens festgelegte Typen für eine imaginäre Berechnungssoftware. Ein Project hat einen Fixpreis, MaintenanceHours hingegen werden nach Stunden und Rate bezahlt.

struct Project { 
    price: f32
}

struct MaintenanceHours {
    hours: f32,
    rate: f32
}

Ein gemeinsamer Trait Billable soll dafür sorgen, dass sich eine Funktion zur Berechnung des Preises definieren lässt, die über alle Typen hinweg implementierbar ist.

trait Billable {
    fn bill(&self) -> f32;
}

Die Schnittstelle verrät, dass sie eine Methode bill implementiert, die auf der Struktur liegt. Das steckt in der Referenz auf self, mit der sich die Funktion wie bei regulären Objekten in klassischen objektorientierten Programmiersprachen verwenden lässt. Die Strukturen liegen großteils am Stack, die dazugehörigen Implementierungsblöcke werden zur Übersetzungszeit entweder ausgewertet oder in leichtgewichtigen Maschinencode übersetzt.

Implementierungen für Billable können wie folgt aussehen:

impl Billable for MaintenanceHours {
    fn bill(&self) -> f32 {
        self.hours * self.rate
    }
}

impl Billable for Project {
    fn bill(&self) -> f32 {
        self.price
    }
}

Traits erlauben zudem, Standardimplementierungen vorzugeben, die sich bei Bedarf durch konkrete Werte austauschen lassen können.

trait Billable {
    fn bill(&self) -> f32 {
        0
    }
}

Etwas Ähnliches tauchte schon in der ersten Ausgabe der Ferris Talks in Form des Iterator Trait auf. Bei ihm muss man nur eine Methode implementieren, die angibt, wie man zum nächsten Element kommt. Der Rest der klassischen Iterator-Funktionsweise baut auf next auf und kann deswegen standardmäßig vorimplementiert werden.

Bis zu diesem Punkt dürfte alles bekannt vorkommen.

betterCode() Rust – Dein Einstieg und Deep Dive in Rust
Heise-Konferenz zu Rust, Einstieg und Deep Dive, 13. Oktober 2021 online, mit Rainer Stropek und Stefan Baumgartner

Heise richtet am 13. Oktober 2021 eine Online-Konferenz zu Rust für Einsteiger und Experten aus. Die beiden Ferris-Talk-Kolumnisten sind dort mit Vorträgen und Workshops präsent:

Rainer Stropek erklärt das Speichermanagement in Rust und erläutert die Rust-Konzepte Ownership, References, Borrowing und Lifetimes. Er zeigt die Konzepte anhand praktischer Code-Beispiele. Wer teilnimmt, erlangt ein Problembewusstsein der Sicherheitsrelevanz von Speicherverwaltung, baut Verständnis auf für die Rust-Kernkonzepte und erkennt Unterschiede zwischen Rust und anderen gängigen Programmiersprachen.

Stefan Baumgartner widmet sich Serverless Rust und führt vor, wie man Serverless-Workloads mit Rust ausführt. Wer teilnimmt, gewinnt ein tieferes Verständnis von AWS Lambda und Azure Functions, lernt die passenden Rust-Crates für Serverless-Workloads und begreift den unmittelbaren Nutzen von Rust dafür.

Flankierende Workshops von Stropek & Baumgartner (9-17 Uhr)

Auch bei der statischen Bindung sind vertraute Muster erkennbar. Die Funktionen müssen erfahren, dass sie mit Billable umgehen können. Das geschieht über sogenannte Trait Bounds, also Abgrenzungen, die mitteilen, welche Eigenschaften innerhalb dieser Funktion nötig sind.

fn print_bill(billable: &impl Billable) {
    println!("{}", billable.bill());
}

Die Funktion ist zu lesen als: Gib mir eine Referenz auf etwas, das Billable implementiert! Kurz noch eine Bemerkung zum Schlüsselwort impl: Das ist ein gutes Beispiel für die schöne Art von Rust, die es erlaubt, Vorhaben explizit anzugeben. Es reicht nicht, den Interface-Namen in die Typ-Annotation zu geben, wie man es von anderen Programmiersprachen gewohnt ist. Es gilt explizit zu sagen, dass man hier einen Trait erwartet.

Über solche expliziten Angaben werden Entwicklerinnen und Entwickler in Rust oft genug stolpern. Sei es bei der Fehlerbehandlung, bei der Typkonvertierung oder bei einem überraschend gängigen Kopiervorgang im Speicher.

Eine alternative Schreibweise für Trait Bounds geht über die Generic-Syntax. Wer oft in TypeScript programmiert, erkennt sicherlich Ähnlichkeiten in der Wortwahl (Bounds) und in der Schreibweise:

fn print_bill<T: Billable>(billable: T) {
    println!("{}", billable.bill());
}

Die Schreibweise ermöglicht es auch, mehrere Trait Bounds zu definieren, falls man doch Funktionalität braucht, die sich nicht in einem Trait beschreiben lässt. Die neue print_bill benötigt an dieser Stelle Methoden aus dem Billable Trait, sowie die Standardausgabe über Display. Ein implementierter Display Trait macht die Strukturen mit dem println!-Makro kompatibel:

fn print_bill<T: Billable + std::fmt::Display>(billable: &T) {
    println!("{}", billable.bill());
    println!("{}", billable); // Dank des Display Traits
}

Doch wie lässt sich der Display Trait implementieren?