Makros in Rust
Auf der Seite Syntax hast du die Faustregel gelernt: Ein ! bedeutet, dass es sich um ein Makro (Macro) handelt und nicht um eine normale Funktion – println!, vec!, format!. Auf dieser Seite erfährst du endlich, was das wirklich bedeutet.
Warum println! keine normale Funktion sein kann
Abschnitt betitelt „Warum println! keine normale Funktion sein kann“Eine normale Funktion wie fn add(a: i32, b: i32) -> i32 nimmt immer eine feste Anzahl von Argumenten mit festen Typen. println! kannst du dagegen mit ganz unterschiedlich vielen Argumenten und ganz unterschiedlichen Typen aufrufen:
fn main() { println!("hier gibt es keine Argumente"); println!("{}", 42); println!("{} und {}", "zwei", 2);}Ausgabe:
hier gibt es keine Argumente42zwei und 2Keine gewöhnliche Funktionssignatur könnte all diese Aufrufe annehmen. Makros funktionieren anders: Sie laufen, bevor dein Code fertig kompiliert ist, und erzeugen echten Rust-Code – passend dazu, wie sie aufgerufen wurden. println!("{} und {}", "zwei", 2) wird im Grunde zu echtem Code erweitert, der genau diese zwei Argumente verarbeitet. Jeder andere Aufruf ergibt eine andere Erweiterung.
Ein einfaches Makro schreiben
Abschnitt betitelt „Ein einfaches Makro schreiben“Eigene Makros definierst du mit macro_rules!. Hier ist eines, das eine Zahl quadriert:
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { println!("{}", square!(5));}Ausgabe:
25($x:expr) bedeutet „nimm einen Ausdruck an und nenne ihn $x“. Der Teil => { ... } ist der Code, zu dem das Makro erweitert wird. Überall, wo du square!(5) schreibst, setzt der Compiler 5 * 5 ein, bevor er überhaupt etwas kompiliert.
Ein Makro mit mehreren Argumenten
Abschnitt betitelt „Ein Makro mit mehreren Argumenten“Makros können auch komplexere Muster erkennen, zum Beispiel eine Liste von Argumenten, die sich wiederholt. Das kann eine normale Funktion schlicht nicht:
macro_rules! sum { ($($x:expr),*) => { { let mut total = 0; $(total += $x;)* total } };}
fn main() { println!("{}", sum!(1, 2, 3)); println!("{}", sum!(10, 20));}Ausgabe:
630$($x:expr),* bedeutet „null oder mehr Ausdrücke, durch Kommas getrennt“. Mit genau diesem Muster funktionieren sowohl vec![1, 2, 3] als auch vec![1, 2, 3, 4, 5] mit ein und derselben Makrodefinition.
Mehr als eine Regel
Abschnitt betitelt „Mehr als eine Regel“Ein Makro kann mehrere Regeln haben, getrennt durch Semikolons. Rust probiert sie von oben nach unten durch und nimmt die erste, die passt. Eine Regel darf das Makro sogar wieder aufrufen. Hier ist ein max!, das mit beliebig vielen Argumenten funktioniert:
macro_rules! max { // Regel 1: nur ein Wert. Der größte ist der Wert selbst. ($x:expr) => { $x }; // Regel 2: ein Wert, danach ein oder mehrere weitere. ($x:expr, $($rest:expr),+) => { { let first = $x; let biggest_of_rest = max!($($rest),+); if first > biggest_of_rest { first } else { biggest_of_rest } } };}
fn main() { println!("{}", max!(7)); println!("{}", max!(3, 9, 4)); println!("{}", max!(2.5, 1.0));}Ausgabe:
792.5Bei max!(3, 9, 4) passt Regel 1 nicht (es ist mehr als ein Wert), also greift Regel 2. Sie legt die 3 beiseite und erweitert max!(9, 4), das wiederum max!(4) erweitert. Das passt auf Regel 1, und dort ist Schluss. Die Reihenfolge ist wichtig: Rust probiert immer zuerst Regel 1.
Hier stecken zwei kleine neue Syntaxteile drin:
+bedeutet ein oder mehr,*(aussum!) dagegen null oder mehr. Regel 2 braucht nach dem Komma mindestens einen Wert. So hat der inneremax!(...)-Aufruf immer etwas, womit er arbeiten kann.max!funktioniert mit Ganzzahlen und mit Gleitkommazahlen, weil das Makro einfach den Codefirst > biggest_of_resteinsetzt. Eine Funktion bräuchte dafür Generics.
expr ist nicht das Einzige, was ein Makro annehmen kann. Ein paar weitere Arten, die dir in echten Makros begegnen:
| Fragment | Passt auf | Beispiel |
|---|---|---|
expr |
einen beliebigen Ausdruck | 2 + 3, name.len() |
ident |
einen Namen | count, Point |
ty |
einen Typ | i32, Vec<String> |
literal |
einen Literalwert | 42, "hi" |
Wo dir Makros wirklich begegnen
Abschnitt betitelt „Wo dir Makros wirklich begegnen“Du benutzt Makros ständig (println!, vec!, format!, #[derive(Debug)]), ohne jemals ein eigenes schreiben zu müssen. Eigene Makros zu schreiben ist eine Spezialfähigkeit. Sie wird vor allem von Bibliotheksautoren genutzt, die in großen Codebasen wirklich repetitiven Boilerplate-Code loswerden wollen. Es ist gut zu wissen, dass es sie gibt und wie sie ungefähr funktionieren – aber am Anfang musst du nicht zu ihnen greifen.
Übungen
Abschnitt betitelt „Übungen“1. Alles ausgeben
Abschnitt betitelt „1. Alles ausgeben“Schreib ein Makro print_all!, das beliebig viele Werte annimmt, durch Kommas getrennt, und jeden davon in einer eigenen Zeile ausgibt. print_all!("Apfel", 42, 3.5) soll drei Zeilen ausgeben. Tipp: Erkenne $($x:expr),* wie bei sum! und wiederhole für jeden Wert ein println! mit $( ... )*.
Lösung anzeigen
macro_rules! print_all { ($($x:expr),*) => { $(println!("{}", $x);)* };}
fn main() { print_all!("Apfel", 42, 3.5);}Ausgabe:
Apfel423.5Das Makro wird zu drei einzelnen println!-Aufrufen erweitert, einem pro Argument. Jeder Wert darf einen anderen Typ haben – das könnte eine normale Funktion nicht.
2. Dein eigenes vec!
Abschnitt betitelt „2. Dein eigenes vec!“Schreib my_vec!, das wie vec! funktioniert. my_vec![] ergibt einen leeren Vec, und my_vec![1, 2, 3] ergibt einen Vec mit diesen Elementen. Tipp: Nimm zwei Regeln. Die erste passt auf gar nichts, also (). Die zweite passt auf $($x:expr),+, legt einen Vec::new() an und schiebt jedes $x mit push hinein.
Lösung anzeigen
macro_rules! my_vec { () => { Vec::new() }; ($($x:expr),+) => { { let mut v = Vec::new(); $(v.push($x);)+ v } };}
fn main() { let empty: Vec<i32> = my_vec![]; let numbers = my_vec![1, 2, 3]; println!("{:?} {:?}", empty, numbers);}Ausgabe:
[] [1, 2, 3]Du kannst jedes Makro mit (), [] oder {} aufrufen. Für listenartige Makros wie vec! nimmt man üblicherweise eckige Klammern, aber my_vec!(1, 2, 3) funktioniert genauso. Der leere Vec braucht den Typ : Vec<i32>, weil Rust ohne Elemente nicht erkennen kann, was darin landen soll.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein Makro wird vor dem Kompilieren zu echtem Code erweitert. Deshalb kann
println!bei jedem Aufruf eine andere Anzahl und andere Typen von Argumenten annehmen. - Ein
!nach einem Namen zeigt immer, dass du ein Makro aufrufst und keine Funktion. - Mit
macro_rules!definierst du eigene Makros, die Muster erkennen – etwa einen einzelnen Ausdruck ($x:expr) oder eine Liste mit Wiederholungen ($($x:expr),*). - Ein Makro kann mehrere Regeln haben. Rust probiert sie von oben nach unten durch, und eine Regel darf das Makro wieder aufrufen.
- Makros zu schreiben ist eine Spezialfähigkeit – mit den Makros aus der Standardbibliothek kommst du fast immer aus.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Warum kann println! nicht als normale Funktion geschrieben werden?
2. Was bedeutet $x:expr in einem macro_rules!-Muster?
3. Wann werden Makros zu echtem Code erweitert?
Punkte: 0 / 3
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Makros – Das Rust-Buch: wie
vec!definiert ist, dazu eine Einführung in prozedurale Makros wie#[derive]. macro_rules!– Rust by Example (auf Englisch): kurze, ausführbare Beispiele zu Designatoren, Überladung und Wiederholung.- Macros By Example – The Rust Reference (auf Englisch): die vollständige Liste der Fragment-Typen wie
expr,identundttund wie das Matching genau funktioniert. - The Little Book of Rust Macros (auf Englisch): ein kostenloser, freundlicher Tiefgang für den Moment, in dem du größere Makros schreiben willst.