Zum Inhalt springen

Fortgeschrittene Generics in Rust: Const Generics, const fn und PhantomData

Auf der Seite Generics war ein generischer Parameter immer ein Typ: Pair<T> konnte i32s oder &strs enthalten. Diese Seite zeigt dir drei Werkzeuge, die dieselbe Idee weiterführen:

  • Const Generics (konstante generische Parameter) machen eine Zahl zum generischen Parameter, wie die 3 in [i32; 3].
  • const fn lässt den Compiler deine Funktion schon beim Bauen des Programms ausführen. Das Ergebnis steht also fest, bevor das Programm startet.
  • PhantomData gibt einem Typ ein Etikett (etwa „das ist eine Benutzer-ID“ oder „diese Länge ist in Fuß“), ohne zusätzliche Daten zu speichern.

Denk an einen Eierkarton. Ein Karton für 6 Eier und einer für 12 Eier sind verschiedene Produkte, obwohl beide „Eierkartons“ sind. Die Größe gehört zu dem, was sie sind. Mit Const Generics kannst du genau das in Rust ausdrücken. Lass dir Zeit mit dieser Seite: Jeder Abschnitt steht für sich, und zu den späteren kannst du zurückkommen, wenn du sie brauchst.

Const Generics: eine Zahl als generischer Parameter

Abschnitt betitelt „Const Generics: eine Zahl als generischer Parameter“

In Rust gehört die Länge eines Arrays zu seinem Typ. [u32; 3] und [u32; 4] sind so verschieden wie i32 und String. Eine Funktion, die [u32; 3] erwartet, nimmt keine vier Elemente an:

error[E0308]: mismatched types
--> src/main.rs:7:26
|
7 | println!("{}", total(basket));
| ----- ^^^^^^ expected an array with a size of 3, found one with a size of 4
| |
| arguments to this function are incorrect

Der Compiler sagt: Er hat ein Array der Größe 3 erwartet, aber eines der Größe 4 bekommen. Ein Const Generic löst das. Du schreibst const N: usize in die spitzen Klammern, und die Funktion funktioniert für ein Array beliebiger Länge:

fn summary<const N: usize>(prices: [u32; N]) -> String {
let mut sum = 0;
for price in prices {
sum += price;
}
// In der Funktion ist N einfach eine ganz normale Zahl
format!("{N} Artikel kosten {sum}")
}
fn main() {
let small_basket = [4, 10, 2];
let big_basket = [3, 8, 1, 12, 6];
println!("{}", summary(small_basket));
println!("{}", summary(big_basket));
}

Ausgabe:

3 Artikel kosten 16
5 Artikel kosten 30
▶ Selbst ausprobieren

Lies <const N: usize> als „für jede Zahl N vom Typ usize“. Rust leitet N aus dem Array ab, das du übergibst – genau wie T bei normalen Generics. Und wie bei normalen Generics erzeugt der Compiler für jede Länge, die du verwendest, eine eigene Kopie von summary. Zur Laufzeit kostet das also nichts.

Ganz ehrlich: Für diese kleine Funktion würde ein Slice (prices: &[u32]) genauso gut funktionieren, und es ist einfacher. Siehe Slices. Richtig lohnen sich Const Generics, wenn die Länge Teil eines Typs sein muss: gespeichert in einem Struct ohne Heap-Allokation oder in einem Rückgabetyp. Darum geht es als Nächstes.

Stell dir den Sitzplan eines Kinos vor: einige Reihen mit jeweils gleich vielen Plätzen. Hier ist eine Matrix, bei der die Anzahl der Zeilen R und der Spalten C beide zum Typ gehören:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new(cells: [[i32; C]; R]) -> Self {
Matrix { cells }
}
fn size(&self) -> String {
format!("{R}x{C}")
}
// Zeilen werden zu Spalten: aus einer R x C-Matrix wird eine C x R-Matrix
fn transpose(&self) -> Matrix<C, R> {
let mut cells = [[0; R]; C];
for row in 0..R {
for col in 0..C {
cells[col][row] = self.cells[row][col];
}
}
Matrix { cells }
}
}
fn main() {
let seats = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let flipped = seats.transpose();
println!("{} {:?}", seats.size(), seats.cells);
println!("{} {:?}", flipped.size(), flipped.cells);
}

Ausgabe:

2x3 [[1, 2, 3], [4, 5, 6]]
3x2 [[1, 4], [2, 5], [3, 6]]
▶ Selbst ausprobieren

Schau dir den Rückgabetyp von transpose an: Matrix<C, R>. Der Compiler weiß, dass aus einer 2x3-Matrix beim Umdrehen eine 3x2-Matrix wird. Würdest du dich darin vertun und eine Matrix mit der falschen Form bauen, ließe sich das Programm nicht kompilieren. Das ganze Raster liegt im Struct selbst – ohne Vec und ohne Heap. Die Zeile impl<const R: usize, const C: usize> funktioniert wie impl<T>: Sie sagt „diese Methoden gibt es für jede Größe“.

Ein Praxisbeispiel: ein Ringpuffer mit fester Größe

Abschnitt betitelt „Ein Praxisbeispiel: ein Ringpuffer mit fester Größe“

Eine Dashcam im Auto behält nur die letzten paar Minuten Video. Wenn sie voll ist, überschreibt die neueste Aufnahme die älteste. Das ist ein Ringpuffer (Ring Buffer), und er passt perfekt zu Const Generics: Die Kapazität steht fest, also kannst du die Elemente in einem Array speichern.

Dieser Puffer ist generisch über beides, einen Typ T und eine Größe N. Deshalb heißt die impl-Zeile impl<T, const N: usize>:

struct RingBuffer<T, const N: usize> {
items: [T; N],
next: usize, // wohin der nächste push geht
len: usize, // wie viele Elemente gespeichert sind (nie mehr als N)
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
fn new() -> Self {
RingBuffer { items: [T::default(); N], next: 0, len: 0 }
}
fn push(&mut self, item: T) {
self.items[self.next] = item; // überschreibt das älteste Element, wenn voll
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
// Die gespeicherten Elemente, das älteste zuerst
fn to_vec(&self) -> Vec<T> {
let start = (self.next + N - self.len) % N;
(0..self.len).map(|i| self.items[(start + i) % N]).collect()
}
}
fn main() {
// Nur die letzten 3 Temperaturmessungen behalten
let mut recent: RingBuffer<i32, 3> = RingBuffer::new();
for temp in [18, 19, 21, 24, 22] {
recent.push(temp);
println!("nach {temp}: {:?}", recent.to_vec());
}
}

Ausgabe:

nach 18: [18]
nach 19: [18, 19]
nach 21: [18, 19, 21]
nach 24: [19, 21, 24]
nach 22: [21, 24, 22]
▶ Selbst ausprobieren

[T::default(); N] füllt das Array mit N Kopien eines Standardwerts (0 bei i32). Damit Rust einen Wert so wiederholen kann, braucht es T: Copy. Deshalb verlangt der impl-Block Copy + Default. Ein RingBuffer<i32, 3> und ein RingBuffer<i32, 60> sind verschiedene Typen. Du kannst also nie einen Puffer mit 3 Plätzen übergeben, wo einer mit 60 erwartet wird. Wenn du lieber die wachsende Variante aus der Standardbibliothek willst, sieh dir VecDeque & BinaryHeap an.

Standardwerte, geschweifte Klammern und heutige Grenzen

Abschnitt betitelt „Standardwerte, geschweifte Klammern und heutige Grenzen“

Ein Const-Parameter kann einen Standardwert haben, geschrieben als const N: usize = 4. Du kannst auch eine benannte Konstante übergeben oder einen Ausdruck in geschweiften Klammern:

struct Buffer<const N: usize = 4> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Self {
Buffer { data: [0; N] }
}
}
const PAGE: usize = 8;
fn main() {
let small: Buffer = Buffer::new(); // keine Zahl: der Standardwert 4
let page = Buffer::<PAGE>::new(); // eine benannte Konstante
let double = Buffer::<{ PAGE * 2 }>::new(); // ein Ausdruck braucht { }
println!("{} {} {}", small.data.len(), page.data.len(), double.data.len());
}

Ausgabe:

4 8 16
▶ Selbst ausprobieren

Der Standardwert greift nur, wenn du den Typ hinschreibst, ohne Zahl, wie bei let small: Buffer. Ein einfaches let small = Buffer::new(); scheitert mit error[E0284]: type annotations needed for Buffer<_> („Typangaben nötig“). Rust leitet aus einem Ausdruck nämlich keinen Standardwert ab.

Const Generics sind in stabilem Rust nützlich, aber bewusst eingeschränkt. Es hilft, die Grenzen zu kennen, bevor du an sie stößt:

  • Erlaubte Typen: nur Ganzzahlen (usize, u8, i32, …), bool und char. Versuchst du const NAME: &'static str, bekommst du „&'static str is forbidden as the type of a const generic parameter“ – dieser Typ ist für einen Const-Parameter verboten.
  • Keine Rechnung mit einem generischen Parameter in einem Typ. [f32; N * 2] in einem generischen Struct oder einer Signatur ist ein Fehler („generic parameters may not be used in const operations“). Ein einfaches N ist in Ordnung, und N * 2 im Rumpf einer Funktion auch – dort ist N einfach ein Wert. Das Feature, das N * 2 in Typen erlauben würde (generic_const_exprs), gibt es bisher nur in Nightly.
  • Einen Wert prüfen. Du kannst keine Schranke wie „N muss größer als 0 sein“ schreiben. Am nächsten kommst du mit einem Inline-const-Block in einer Funktion: const { assert!(N > 0, "need at least one slot") };. Er schlägt fehl, wenn du das Programm mit einem ungültigen N baust (cargo check merkt es eventuell nicht).

Manche Läden drucken ihre Preislisten einmal aus, statt jeden Preis an der Kasse neu auszurechnen. Eine const fn ist eine Funktion, die der Compiler beim Bauen deines Programms ausführen darf. Das Ergebnis wird also schon vorab ins Programm „gedruckt“.

const-Items kennst du schon von der Seite Variablen. Ihr Wert muss zur Compile-Zeit bekannt sein, deshalb sind dort normalerweise nur Literale und einfache Rechnungen erlaubt. Mit const fn kannst du dort auch deine eigenen Funktionen aufrufen:

const fn square(n: usize) -> usize {
n * n
}
const BOARD_SIDE: usize = 4;
const CELLS: usize = square(BOARD_SIDE); // vom Compiler ausgerechnet
fn main() {
// Array-Längen müssen zur Compile-Zeit bekannt sein, und CELLS ist es
let board = [0u8; CELLS];
println!("ein {BOARD_SIDE}x{BOARD_SIDE}-Brett hat {} Felder", board.len());
println!("square(12) zur Laufzeit ist {}", square(12));
}

Ausgabe:

ein 4x4-Brett hat 16 Felder
square(12) zur Laufzeit ist 144
▶ Selbst ausprobieren

Eine const fn ist trotzdem eine ganz normale Funktion. square(12) in main ist ein gewöhnlicher Aufruf zur Laufzeit. Zur Compile-Zeit laufen muss sie nur, wenn du sie in einem Const-Kontext verwendest: in einem const- oder static-Item, einer Array-Länge, einem Const-Generic-Argument oder einem const { ... }-Block.

Ein klassischer Einsatz ist eine Lookup-Tabelle (Nachschlagetabelle): Du rechnest eine Liste von Werten einmal zur Compile-Zeit aus und liest sie zur Laufzeit nur noch. Hier baut der Compiler die Tabelle „an welchem Tag des Jahres beginnt jeder Monat“ für ein Jahr ohne Schalttag:

const DAYS_IN_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const fn days_before_each_month() -> [u32; 12] {
let mut table = [0; 12];
let mut month = 1;
while month < 12 {
table[month] = table[month - 1] + DAYS_IN_MONTH[month - 1];
month += 1;
}
table
}
// Einmal vom Compiler gebaut. Zur Laufzeit sind es nur 12 Zahlen.
const DAYS_BEFORE: [u32; 12] = days_before_each_month();
fn day_of_year(month: usize, day: u32) -> u32 {
DAYS_BEFORE[month - 1] + day
}
fn main() {
println!("{:?}", DAYS_BEFORE);
println!("Der 1. März ist Tag {}", day_of_year(3, 1));
println!("Der 25. Dezember ist Tag {}", day_of_year(12, 25));
}

Ausgabe:

[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
Der 1. März ist Tag 60
Der 25. Dezember ist Tag 359
▶ Selbst ausprobieren

Fällt dir die while-Schleife statt einer for-Schleife auf? Das ist keine Geschmacksfrage. Das hier darfst du heute in einer const fn verwenden:

  • let und let mut, Rechenoperationen, Vergleiche, if, match, loop und while
  • Arrays, Tupel, Structs und Enums sowie Indexzugriffe wie table[month]
  • Aufrufe anderer const fns (viele aus der Standardbibliothek sind const, etwa u32::pow oder slice.len())
  • &mut-Referenzen und Gleitkomma-Rechnungen (stabil seit Rust 1.83 bzw. 1.82)
  • panic! und assert! – sie werden zu Compilerfehlern, wenn sie zur Compile-Zeit auslösen

Und das geht noch nicht: for-Schleifen (sie brauchen den Trait Iterator, und Trait-Methoden sind in stabilem Rust nicht const), Aufrufe normaler Funktionen wie String::from oder println! und alles, was Speicher auf dem Heap anlegt, zum Beispiel ein push auf einen Vec.

const oder static? Beides sind globale Werte, die sich nie ändern. Sie werden aber unterschiedlich gespeichert:

const static
Wo er liegt wird an jede Stelle kopiert, an der du ihn verwendest an einer festen Stelle im Speicher
Adresse kann jedes Mal anders sein immer dieselbe, lebt so lange wie das Programm ('static)
Veränderbar? nie static mut gibt es, braucht aber unsafe
Wofür Zahlen, kleine Tabellen, Einstellungen eine große Tabelle, die nur einmal gespeichert werden soll, oder ein globaler Wert, auf den du eine Referenz brauchst

Im Zweifel nimm const. Brauchst du einen globalen Wert, den du ändern kannst, dann nimm statt static mut ein static mit einem Atomic oder einem Mutex darin. Siehe Gemeinsamer Zustand.

PhantomData: ein Etikett, das du nicht verwechseln kannst

Abschnitt betitelt „PhantomData: ein Etikett, das du nicht verwechseln kannst“

Dieser Fehler passiert in echten Systemen ständig: Benutzer-IDs und Bestell-IDs sind beides nur Zahlen. Nichts hindert dich also daran, eine Benutzer-ID an cancel_order zu übergeben. Schöner wäre es, wenn der Typ ein Etikett trägt: Id<User> gegenüber Id<Order>. Dein erster Versuch sieht vielleicht so aus:

struct Id<T> {
value: u32,
}
error[E0392]: type parameter `T` is never used
--> src/main.rs:1:11
|
1 | struct Id<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

Rust lehnt einen Typparameter ab, den kein Feld benutzt („type parameter T is never used“). Die Lösung ist die, die der Compiler vorschlägt. std::marker::PhantomData<T> ist ein Feld, das sagt „dieses Struct gehört zu T“, aber nichts enthält. Es belegt null Bytes:

use std::marker::PhantomData;
struct User;
struct Order;
struct Id<T> {
value: u32,
_marker: PhantomData<T>, // null Bytes, nur ein Etikett
}
impl<T> Id<T> {
fn new(value: u32) -> Self {
Id { value, _marker: PhantomData }
}
}
fn find_user(id: &Id<User>) -> &'static str {
if id.value == 7 { "Amara" } else { "unbekannt" }
}
fn cancel_order(id: &Id<Order>) {
println!("storniere Bestellung #{}", id.value);
}
fn main() {
let user_id: Id<User> = Id::new(7);
let order_id: Id<Order> = Id::new(7);
println!("Benutzer #{} ist {}", user_id.value, find_user(&user_id));
cancel_order(&order_id);
println!("eine Id ist {} Bytes groß", std::mem::size_of::<Id<User>>());
}

Ausgabe:

Benutzer #7 ist Amara
storniere Bestellung #7
eine Id ist 4 Bytes groß
▶ Selbst ausprobieren

Beide IDs enthalten die Zahl 7, und beide sind 4 Bytes groß – genau wie ein einfaches u32. Füge jetzt aber cancel_order(&user_id); als letzte Zeile von main hinzu:

error[E0308]: mismatched types
--> src/main.rs:32:18
|
32 | cancel_order(&user_id);
| ------------ ^^^^^^^^ expected `&Id<Order>`, found `&Id<User>`
| |
| arguments to this function are incorrect

Rust erwartet eine &Id<Order>, bekommt aber eine &Id<User>. Die Verwechslung fällt auf, bevor das Programm überhaupt läuft. PhantomData ist ein Wert und gleichzeitig ein Typ. Deshalb schreibst du _marker: PhantomData, wenn du das Struct baust. Der Unterstrich am Anfang ist nur eine verbreitete Namenskonvention für „dieses Feld liest du nie“.

1999 verlor die NASA den Mars Climate Orbiter, weil eine Software Zahlen in imperialen Einheiten lieferte und eine andere metrische erwartete. Beides waren „nur Zahlen“. Mit PhantomData wird die Einheit Teil des Typs, und ein einziges generisches Length<U> deckt jede Einheit ab:

use std::marker::PhantomData;
use std::ops::Add;
struct Meters;
struct Feet;
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
}
// Ein impl für jede Einheit, aber beide Seiten müssen dieselbe Einheit haben
impl<U> Add for Length<U> {
type Output = Length<U>;
fn add(self, other: Length<U>) -> Length<U> {
Length::new(self.value + other.value)
}
}
impl Length<Feet> {
fn to_meters(self) -> Length<Meters> {
Length::new(self.value * 0.3048)
}
}
fn main() {
let run: Length<Meters> = Length::new(400.0) + Length::new(250.0);
let climb: Length<Feet> = Length::new(1000.0);
let climb_m = climb.to_meters();
println!("Aufstieg: {:.1} m", climb_m.value);
let total = run + climb_m;
println!("gesamt: {:.1} m", total.value);
}

Ausgabe:

Aufstieg: 304.8 m
gesamt: 954.8 m
▶ Selbst ausprobieren

Schreibst du run + climb (und vergisst to_meters()), weigert sich Rust:

error[E0308]: mismatched types
--> src/main.rs:39:23
|
39 | let total = run + climb;
| ^^^^^ expected `Length<Meters>`, found `Length<Feet>`

Rust erwartet Meter, bekommt aber Fuß. Auf der Seite Fortgeschrittene Traits waren Meters und Centimeters zwei getrennte Structs, jedes mit einem eigenen Add-impl. Hier reicht ein einziges impl<U> Add for Length<U> für jede Einheit, die du jemals addieren wirst.

Typestate: eine Tür, die sich offen nicht abschließen lässt

Abschnitt betitelt „Typestate: eine Tür, die sich offen nicht abschließen lässt“

Mit demselben Trick kannst du auch den Zustand von etwas verfolgen. Dieses Muster heißt Typestate (Zustand im Typ). Eine Tür kann offen, geschlossen oder abgeschlossen sein. Eine geschlossene Tür kannst du abschließen, aber eine offene Tür abzuschließen ergibt keinen Sinn. Steht der Zustand als Etikett im Typ, gibt es ungültige Schritte einfach nicht:

use std::marker::PhantomData;
struct Open;
struct Closed;
struct Locked;
struct Door<State> {
room: String,
_state: PhantomData<State>,
}
// In jedem Zustand verfügbar
impl<State> Door<State> {
fn room(&self) -> &str {
&self.room
}
// Dieselbe Tür, neues Zustandsetikett
fn into_state<Next>(self, action: &str) -> Door<Next> {
println!("{}: {action}", self.room);
Door { room: self.room, _state: PhantomData }
}
}
impl Door<Closed> {
fn new(room: &str) -> Self {
Door { room: room.to_string(), _state: PhantomData }
}
fn open(self) -> Door<Open> {
self.into_state("geöffnet")
}
fn lock(self) -> Door<Locked> {
self.into_state("abgeschlossen")
}
}
impl Door<Open> {
fn close(self) -> Door<Closed> {
self.into_state("geschlossen")
}
}
impl Door<Locked> {
fn unlock(self) -> Door<Closed> {
self.into_state("aufgeschlossen")
}
}
fn main() {
let door = Door::new("Serverraum").open().close().lock();
println!("{} ist über Nacht abgeschlossen", door.room());
let door = door.unlock().open();
println!("{} ist wieder offen", door.room());
}

Ausgabe:

Serverraum: geöffnet
Serverraum: geschlossen
Serverraum: abgeschlossen
Serverraum ist über Nacht abgeschlossen
Serverraum: aufgeschlossen
Serverraum: geöffnet
Serverraum ist wieder offen
▶ Selbst ausprobieren

Versuch jetzt, eine offene Tür abzuschließen, Door::new("Serverraum").open().lock():

error[E0599]: no method named `lock` found for struct `Door<Open>` in the current scope
--> src/main.rs:50:47
|
7 | struct Door<State> {
| ------------------ method `lock` not found for this struct
...
50 | let door = Door::new("Serverraum").open().lock();
| ^^^^ method not found in `Door<Open>`
|
= note: the method was found for `Door<Closed>`

Die Meldung sagt: Für Door<Open> gibt es keine Methode lock. Die Notiz ergänzt, dass es diese Methode für Door<Closed> sehr wohl gibt. Jede Methode nimmt self als Wert. Die alte Tür ist danach also verbraucht, und du kannst eine „geschlossene“ Tür nicht weiterbenutzen, nachdem du sie geöffnet hast. Die Seite Entwurfsmuster zeigt dieselbe Typestate-Idee, bei der jeder Zustand zusätzlich eigene Daten enthält. PhantomData ist die leichtere Variante für den Fall, dass die Zustände reine Etiketten sind.

Methoden für eine bestimmte Größe und assoziierte Konstanten

Abschnitt betitelt „Methoden für eine bestimmte Größe und assoziierte Konstanten“

Auf der Seite Generics hast du impl Pair<f64> gesehen: Methoden nur für einen konkreten Typ. Mit Const Generics funktioniert das genauso. Manche Methoden ergeben nur für bestimmte Größen Sinn:

struct Matrix<const R: usize, const C: usize> {
cells: [[i32; C]; R],
}
// Nur quadratische Matrizen: Zeilen und Spalten sind dasselbe N
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> i32 {
let mut sum = 0;
for i in 0..N {
sum += self.cells[i][i];
}
sum
}
}
// Nur 2x2-Matrizen
impl Matrix<2, 2> {
fn determinant(&self) -> i32 {
let [[a, b], [c, d]] = self.cells;
a * d - b * c
}
}
fn main() {
let small = Matrix { cells: [[3, 8], [4, 6]] };
let big = Matrix { cells: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] };
println!("klein: Spur {}, Determinante {}", small.trace(), small.determinant());
println!("groß: Spur {}", big.trace());
}

Ausgabe:

klein: Spur 9, Determinante -14
groß: Spur 15
▶ Selbst ausprobieren

big.determinant() lässt sich nicht kompilieren, und trace() auf einer 2x3-Matrix genauso wenig. Du bekommst Fehler wie error[E0599]: no method named trace found for struct Matrix<2, 3> mit der Notiz the method was found for Matrix<N, N>. Die Regel „nur quadratische Matrizen haben eine Spur“ steht jetzt in den Typen.

Eine assoziierte Konstante (Associated Const) ist eine Konstante, die zu einem Typ oder einem Trait gehört, so wie eine Methode. In einem Trait wählt jede Implementierung ihren eigenen Wert, und generischer Code kann ihn als U::NAME lesen. Geben wir den Einheiten von vorhin ein Symbol und eine Größe:

use std::fmt;
use std::marker::PhantomData;
trait Unit {
const SYMBOL: &'static str;
const IN_METERS: f64; // wie viele Meter eine dieser Einheiten sind
}
struct Meters;
struct Feet;
struct Miles;
impl Unit for Meters {
const SYMBOL: &'static str = "m";
const IN_METERS: f64 = 1.0;
}
impl Unit for Feet {
const SYMBOL: &'static str = "ft";
const IN_METERS: f64 = 0.3048;
}
impl Unit for Miles {
const SYMBOL: &'static str = "mi";
const IN_METERS: f64 = 1609.344;
}
struct Length<U> {
value: f64,
_unit: PhantomData<U>,
}
impl<U: Unit> Length<U> {
fn new(value: f64) -> Self {
Length { value, _unit: PhantomData }
}
// Funktioniert zwischen zwei beliebigen Einheiten, mithilfe ihrer Konstanten
fn convert<To: Unit>(&self) -> Length<To> {
Length::new(self.value * U::IN_METERS / To::IN_METERS)
}
}
impl<U: Unit> fmt::Display for Length<U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.2} {}", self.value, U::SYMBOL)
}
}
fn main() {
let marathon: Length<Miles> = Length::new(26.2);
let everest: Length<Feet> = Length::new(29032.0);
println!("{} = {}", marathon, marathon.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Meters>());
println!("{} = {}", everest, everest.convert::<Miles>());
}

Ausgabe:

26.20 mi = 42164.81 m
29032.00 ft = 8848.95 m
29032.00 ft = 5.50 mi
▶ Selbst ausprobieren

U::IN_METERS wird zur Compile-Zeit nachgeschlagen. Zur Laufzeit muss also keine Tabelle durchsucht werden. Eine neue Einheit, etwa Kilometers, ist ein kleiner impl Unit-Block, und convert und Display funktionieren sofort damit. Auch ein normaler (inhärenter) impl-Block kann assoziierte Konstanten haben, zum Beispiel impl<T, const N: usize> RingBuffer<T, N> { const CAPACITY: usize = N; }, gelesen als RingBuffer::<i32, 3>::CAPACITY.

Wenn du eine Trait-Implementierung für jeden Typ willst, der eine Bedingung erfüllt, ist das ein Blanket Impl. Den findest du auf der Seite Fortgeschrittene Traits.

Diese Features sind mächtig, und genau das macht sie verlockend. Ein paar ehrliche Ratschläge:

  • Oft reicht ein Slice oder ein Vec. Wenn eine Funktion Elemente nur liest, nimmt &[T] jede Länge an und ist leichter zu lesen. Wenn die Länge erst zur Laufzeit bekannt ist (eine Datei, Benutzereingaben), brauchst du sowieso einen Vec.
  • Große Arrays liegen auf dem Stack. Ein RingBuffer<u8, 10_000_000> ist zehn Megabyte groß – direkt im Struct. Das kann den Stack sprengen. Pack ihn in eine Box (siehe Smart Pointer) oder nimm einen Vec.
  • Jede neue Größe und jeder neue Typ ist eine neue Kopie des Codes. Verwendest du ein Generic mit 20 verschiedenen Werten für N, entstehen 20 Versionen. Das kostet Compile-Zeit und macht das Binary größer. Siehe Performance.
  • Typestate bringt mehr Typen und längere Fehlermeldungen mit sich. Für eine API, die viele Leute nutzen (ein Builder, eine Verbindung, ein Bezahlvorgang), ist das großartig. Für einen Zustand, der sich nach Laufzeitdaten richtet, etwa einen Status aus einer Datenbank, nimm lieber ein Enum und match.

Eine gute Faustregel: Fang einfach an. Greif zu diesen Werkzeugen, wenn ein echter Fehler oder echte Kosten auftauchen, die sie verhindern würden.

Mit einem Const-Parameter in einem Typ rechnen. Das fühlt sich natürlich an, aber stabiles Rust erlaubt es noch nicht:

struct Pairs<const N: usize> {
values: [f32; N * 2],
}
error: generic parameters may not be used in const operations
--> src/main.rs:2:19
|
2 | values: [f32; N * 2],
| ^ cannot perform const operation using `N`
|
= help: const parameters may only be used as standalone arguments here, i.e. `N`

Der Compiler sagt: Generische Parameter dürfen nicht in Const-Rechnungen vorkommen, hier ist nur ein alleinstehendes N erlaubt. Lösungen: Nimm [[f32; 2]; N] (ein Array aus Paaren), füge einen zweiten Parameter hinzu oder verwende einen Vec, wenn die Größe nicht im Typ stehen muss.

Die geschweiften Klammern um einen Ausdruck vergessen. Buffer::<2 + 2>::new() ergibt error: expressions must be enclosed in braces to be used as const generic arguments (Ausdrücke müssen in geschweiften Klammern stehen), und die Hilfezeile zeigt die Lösung: Buffer::<{ 2 + 2 }>::new(). Eine einfache Zahl oder der Name einer const braucht keine Klammern.

In einem Const-Kontext etwas aufrufen, das nicht const ist. Eine const kann keine normale Funktion aufrufen, und eine const fn kann keine for-Schleife verwenden:

const GREETING: String = String::from("hello");
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
--> src/main.rs:1:26
|
1 | const GREETING: String = String::from("hello");
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: calls in constants are limited to constant functions, tuple structs and tuple variants

Die Meldung heißt: String::from ist keine const-Funktion, und in Konstanten sind nur Aufrufe von const-Funktionen erlaubt. Für Text nimmst du const GREETING: &str = "hello";. In einer const fn ersetzt du for durch eine while-Schleife mit Index, wie bei der Lookup-Tabelle oben.

#[derive(Clone, Copy)] auf einem Struct mit PhantomData. Du würdest erwarten, dass Id<User> Copy ist, denn es enthält ja nur ein u32. Aber derive fügt die Schranke T: Copy hinzu, und User ist nicht Copy. Deshalb wird die ID verschoben (Move) statt kopiert:

error[E0382]: use of moved value: `id`
--> src/main.rs:16:10
|
14 | let id: Id<User> = Id { value: 1, _marker: PhantomData };
| -- move occurs because `id` has type `Id<User>`, which does not implement the `Copy` trait
15 | show(id);
| -- value moved here
16 | show(id);
| ^^ value used here after move
|
note: if all bounds were met, you could clone the value
--> src/main.rs:4:1
|
3 | #[derive(Clone, Copy)]
| ----- derived `Clone` adds implicit bounds on type parameters

Die Notiz am Ende verrät die Ursache: Das abgeleitete Clone fügt den Typparametern versteckte Schranken hinzu. Die einfachste Lösung: Leite Clone, Copy auch für die Marker-Typen ab (#[derive(Clone, Copy)] struct User;). Oder schreib die Impls von Hand – die brauchen keine Schranke für T:

impl<T> Clone for Id<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Id<T> {}

Schreib fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N]. Die Funktion kopiert die Punktzahlen in ein Array mit genau N Elementen. Fehlende Plätze füllst du mit 0, überzählige Punktzahlen fallen weg. Ruf sie als pad_scores::<5>(&[90, 85]) auf und als let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);. Tipp: Fang mit [0; N] an, lauf dann über 0..N und nimm scores.get(i).

Lösung anzeigen
fn pad_scores<const N: usize>(scores: &[u32]) -> [u32; N] {
let mut out = [0; N];
for i in 0..N {
if let Some(&score) = scores.get(i) {
out[i] = score;
}
}
out
}
fn main() {
let five = pad_scores::<5>(&[90, 85]);
let top3: [u32; 3] = pad_scores(&[70, 95, 88, 60]);
println!("{:?}", five);
println!("{:?}", top3);
}

Ausgabe:

[90, 85, 0, 0, 0]
[70, 95, 88]
▶ Selbst ausprobieren

Hier kann N nicht aus dem Argument kommen (ein Slice hat keine Länge in seinem Typ). Es kommt deshalb aus dem Turbofish ::<5> oder aus dem Typ, den du verlangst: [u32; 3].

Schreib zwei const fns, minutes(n: u64) -> u64 und hours(n: u64) -> u64, die eine Anzahl Sekunden zurückgeben. Definiere damit const SESSION_TIMEOUT: u64 = hours(1) + minutes(30); und ein Array const RETRY_DELAYS: [u64; 3] mit 1, 5 und 15 Minuten. Gib beides aus. Tipp: hours darf minutes aufrufen.

Lösung anzeigen
const fn minutes(n: u64) -> u64 {
n * 60
}
const fn hours(n: u64) -> u64 {
minutes(n * 60)
}
const SESSION_TIMEOUT: u64 = hours(1) + minutes(30);
const RETRY_DELAYS: [u64; 3] = [minutes(1), minutes(5), minutes(15)];
fn main() {
println!("Session-Timeout: {SESSION_TIMEOUT} Sekunden");
println!("Wartezeiten vor neuen Versuchen: {:?}", RETRY_DELAYS);
}

Ausgabe:

Session-Timeout: 5400 Sekunden
Wartezeiten vor neuen Versuchen: [60, 300, 900]
▶ Selbst ausprobieren

Der Compiler rechnet 5400 und die Tabelle mit den Wartezeiten schon beim Bauen aus. Und hours(1) + minutes(30) liest sich viel besser als eine nackte 5400. Entfernst du const bei minutes, lassen sich die const-Items nicht mehr kompilieren, und du bekommst E0015.

Leg struct Euro; und struct Dollar; an und dazu ein Money<C> mit einem Feld cents: i64 und einem Feld PhantomData<C>. Gib ihm new(cents) und plus(self, other: Money<C>) -> Money<C>. Addiere ein Mittagessen und einen Kaffee in Euro und gib die Summe aus. Tipp: Weil other dasselbe C hat, lassen sich ein Money<Euro> und ein Money<Dollar> nicht addieren.

Lösung anzeigen
use std::marker::PhantomData;
struct Euro;
struct Dollar;
struct Money<C> {
cents: i64,
_currency: PhantomData<C>,
}
impl<C> Money<C> {
fn new(cents: i64) -> Self {
Money { cents, _currency: PhantomData }
}
fn plus(self, other: Money<C>) -> Money<C> {
Money::new(self.cents + other.cents)
}
}
fn main() {
let lunch: Money<Euro> = Money::new(1250);
let coffee: Money<Euro> = Money::new(320);
let ticket: Money<Dollar> = Money::new(4999);
let total = lunch.plus(coffee);
println!("gesamt: {}.{:02} EUR", total.cents / 100, total.cents % 100);
println!("Ticket: {}.{:02} USD", ticket.cents / 100, ticket.cents % 100);
}

Ausgabe:

gesamt: 15.70 EUR
Ticket: 49.99 USD
▶ Selbst ausprobieren

Probier lunch.plus(ticket): Das schlägt fehl mit expected Money<Euro>, found Money<Dollar>. Dass die Cents als Ganzzahl statt als f64 gespeichert sind, vermeidet übrigens Rundungsfehler bei Geldbeträgen.

  • Const Generics (const N: usize) machen eine Zahl zum Teil eines Typs. Arrays verschiedener Länge sind verschiedene Typen, und mit impl<T, const N: usize> schreibst du Methoden für jede Größe.
  • In stabilem Rust dürfen Const-Parameter Ganzzahlen, bool oder char sein. Ausdrücke als Argumente brauchen geschweifte Klammern ({ PAGE * 2 }), und Rechnungen mit einem generischen N in einem Typ sind noch nicht erlaubt.
  • Eine const fn kann in Const-Kontexten (const, static, Array-Längen) zur Compile-Zeit laufen. Nimm while statt for, und ruf nur andere const fns auf.
  • PhantomData<T> ist ein Feld ohne Größe, das ein Struct mit einem Typ verbindet, den es nicht speichert. Du nutzt es für typisierte IDs, Maßeinheiten und Typestate.
  • impl Matrix<2, 2> und impl<const N: usize> Matrix<N, N> geben nur bestimmten Größen Methoden. Assoziierte Konstanten wie U::IN_METERS geben jedem Typ seinen eigenen Wert zur Compile-Zeit.
  • Diese Werkzeuge kosten Compile-Zeit und Lesbarkeit. Setz sie ein, wenn sie einen echten Fehler verhindern.

Kurzer Check

1. Warum kannst du einer Funktion, die [u32; 3] erwartet, kein [u32; 4] übergeben?

2. Was fügt PhantomData<T> einem Struct zur Laufzeit hinzu?

3. Was davon kannst du in stabilem Rust in einer const fn NICHT verwenden?

4. Was bedeutet impl<const N: usize> Matrix<N, N>?

Punkte: 0 / 4