Szukaj…
Uwagi
Omówienie makr można znaleźć w The Rust Programming Language (alias The Book) .
Instruktaż
Makra pozwalają nam abstrakcyjnie powtarzać wzorce syntaktyczne. Na przykład:
/// Computes `a + b * c`. If any of the operation overflows, returns `None`.
fn checked_fma(a: u64, b: u64, c: u64) -> Option<u64> {
let product = match b.checked_mul(c) {
Some(p) => p,
None => return None,
};
let sum = match a.checked_add(product) {
Some(s) => s,
None => return None,
};
Some(sum)
}
Zauważamy, że dwie instrukcje match
są bardzo podobne: obie mają ten sam wzór
match expression {
Some(x) => x,
None => return None,
}
Wyobraźmy sobie, że reprezentujemy powyższy wzorzec jako try_opt!(expression)
, a następnie moglibyśmy przepisać funkcję na 3 linie:
fn checked_fma(a: u64, b: u64, c: u64) -> Option<u64> {
let product = try_opt!(b.checked_mul(c));
let sum = try_opt!(a.checked_add(product));
Some(sum)
}
try_opt!
nie można napisać funkcji, ponieważ funkcja nie obsługuje wcześniejszego powrotu. Ale możemy to zrobić za pomocą makra - ilekroć mamy te wzorce składniowe, których nie można przedstawić za pomocą funkcji, możemy spróbować użyć makra.
Definiujemy makro za pomocą macro_rules!
składnia:
macro_rules! try_opt {
// ^ note: no `!` after the macro name
($e:expr) => {
// ^~~~~~~ The macro accepts an "expression" argument, which we call `$e`.
// All macro parameters must be named like `$xxxxx`, to distinguish from
// normal tokens.
match $e {
// ^~ The input is used here.
Some(x) => x,
None => return None,
}
}
}
Otóż to! Stworzyliśmy nasze pierwsze makro.
(Wypróbuj w Rust Playground )
Utwórz makro HashSet
// This example creates a macro `set!` that functions similarly to the built-in
// macro vec!
use std::collections::HashSet;
macro_rules! set {
( $( $x:expr ),* ) => { // Match zero or more comma delimited items
{
let mut temp_set = HashSet::new(); // Create a mutable HashSet
$(
temp_set.insert($x); // Insert each item matched into the HashSet
)*
temp_set // Return the populated HashSet
}
};
}
// Usage
let my_set = set![1, 2, 3, 4];
Rekurencja
Makro może się wywoływać, podobnie jak rekurencja funkcji:
macro_rules! sum {
($base:expr) => { $base };
($a:expr, $($rest:expr),+) => { $a + sum!($($rest),+) };
}
Przejdźmy jednak do rozszerzenia sum!(1, 2, 3)
:
sum!(1, 2, 3)
// ^ ^~~~
// $a $rest
=> 1 + sum!(2, 3)
// ^ ^
// $a $rest
=> 1 + (2 + sum!(3))
// ^
// $base
=> 1 + (2 + (3))
Limit rekurencji
Gdy kompilator zbyt głęboko rozwija makra, zrezygnuje. Domyślnie kompilator zawiedzie po rozszerzeniu makr do 64 poziomów głębokości, więc np. Następujące rozszerzenie spowoduje awarię:
sum!(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,
21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62)
// error: recursion limit reached while expanding the macro `sum`
// --> <anon>:3:46
// 3 |> ($a:expr, $($rest:expr),+) => { $a + sum!($($rest),+) };
// |> ^^^^^^^^^^^^^^^^
Po osiągnięciu limitu rekurencji należy rozważyć refaktoryzację makra, np
- Może rekurencję można zastąpić powtórzeniem?
- Może format wejściowy może zostać zmieniony na mniej wyszukany, więc nie potrzebujemy rekurencji, aby go dopasować?
Jeśli istnieje uzasadniony powód, dla którego poziomy 64 nie są wystarczające, zawsze możesz zwiększyć limit skrzynki wywołującej makro za pomocą atrybutu:
#![recursion_limit="128"]
// ^~~ set the recursion limit to 128 levels deep.
Wiele wzorów
Makro może wytwarzać różne dane wyjściowe dla różnych wzorców wejściowych:
/// The `sum` macro may be invoked in two ways:
///
/// sum!(iterator)
/// sum!(1234, iterator)
///
macro_rules! sum {
($iter:expr) => { // This branch handles the `sum!(iterator)` case
$iter.fold(0, |a, b| a + *b)
};
// ^ use `;` to separate each branch
($start:expr, $iter:expr) => { // This branch handles the `sum!(1234, iter)` case
$iter.fold($start, |a, b| a + *b)
};
}
fn main() {
assert_eq!(10, sum!([1, 2, 3, 4].iter()));
assert_eq!(23, sum!(6, [2, 5, 9, 1].iter()));
}
Specyfikatory fragmentów - rodzaj wzorów
W $e:expr
expr
nazywa się specyfikatorem fragmentu . Mówi parserowi, jakiego rodzaju tokenów oczekuje parametr $e
. Rdza zapewnia różnorodne specyfikatory fragmentów, dzięki czemu dane wejściowe są bardzo elastyczne.
Specyficzny | Opis | Przykłady |
---|---|---|
ident | Identyfikator | x , foo |
path | Imię i nazwisko | std::collection::HashSet , Vec::new |
ty | Rodzaj | i32 , &T , Vec<(char, String)> |
expr | Wyrażenie | 2+2 , f(42) , if true { 1 } else { 2 } |
pat | Wzór | _ , c @ 'a' ... 'z' , (true, &x) , Badger { age, .. } |
stmt | Komunikat | let x = 3 , return 42 |
block | Blok rozdzielany nawiasami klamrowymi | { foo(); bar(); } , { x(); y(); z() } |
item | Pozycja | fn foo() {} , struct Bar; , use std::io; |
meta | Wewnątrz atrybutu | cfg!(windows) , doc="comment" |
tt | Drzewo żetonów | + , foo , 5 , [?!(???)] |
Zauważ, że komentarz do dokumentu /// comment
jest traktowany tak samo jak #[doc="comment"]
do makra.
macro_rules! declare_const_option_type {
(
$(#[$attr:meta])*
const $name:ident: $ty:ty as optional;
) => {
$(#[$attr])*
const $name: Option<$ty> = None;
}
}
declare_const_option_type! {
/// some doc comment
const OPT_INT: i32 as optional;
}
// The above will be expanded to:
#[doc="some doc comment"]
const OPT_INT: Option<i32> = None;
Śledź zestaw
Niektóre specyfikatory fragmentów wymagają, aby następujący token był jednym z ograniczonego zestawu, zwanego „zestawem śledzenia”. Pozwala to na pewną elastyczność w rozwijaniu składni Rust bez przerywania istniejących makr.
Specyficzny | Śledź zestaw |
---|---|
expr , stmt | => , ; |
ty , path | => , = | ; : > [ { as where |
pat | => , = | if in |
ident , block , item , meta , tt | dowolny token |
macro_rules! invalid_macro {
($e:expr + $f:expr) => { $e + $f };
// ^
// `+` is not in the follow set of `expr`,
// and thus the compiler will not accept this macro definition.
($($e:expr)/+) => { $($e)/+ };
// ^
// The separator `/` is not in the follow set of `expr`
// and thus the compiler will not accept this macro definition.
}
Eksportowanie i importowanie makr
Eksportowanie makra, aby umożliwić innym modułom korzystanie z niego:
#[macro_export]
// ^~~~~~~~~~~~~~~ Think of it as `pub` for macros.
macro_rules! my_macro { (..) => {..} }
Używanie makr z innych skrzynek lub modułów:
#[macro_use] extern crate lazy_static;
// ^~~~~~~~~~~~ Must add this in order to use macros from other crates
#[macro_use] mod macros;
// ^~~~~~~~~~~~ The same for child modules.
Debugowanie makr
(Wszystkie z nich są niestabilne i dlatego można ich używać tylko z nocnego kompilatora).
log_syntax! ()
#![feature(log_syntax)]
macro_rules! logged_sum {
($base:expr) => {
{ log_syntax!(base = $base); $base }
};
($a:expr, $($rest:expr),+) => {
{ log_syntax!(a = $a, rest = $($rest),+); $a + logged_sum!($($rest),+) }
};
}
const V: u32 = logged_sum!(1, 2, 3);
Podczas kompilacji wypisze na standardowe wyjście:
a = 1, reszta = 2, 3
a = 2, reszta = 3
podstawa = 3
- ładny rozszerzony
Uruchom kompilator za pomocą:
rustc -Z unstable-options --pretty expanded filename.rs
Spowoduje to rozwinięcie wszystkich makr, a następnie wydrukuje rozwinięty wynik na standardowe wyjście, np. Powyższe prawdopodobnie wygeneruje:
#![feature(prelude_import)]
#![no_std]
#![feature(log_syntax)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
const V: u32 = { false; 1 + { false; 2 + { false; 3 } } };
(Jest to podobne do flagi -E
w kompilatorach C gcc
i clang
.)