编写宏

编写宏是非常复杂的。你可能永远都不需要写宏,但有时你可能会想写,因为它们非常方便。写宏很有趣,因为它们几乎是不同的语言。要写一个宏,你实际上是用另一个叫macro_rules!的宏。然后你添加你的宏名称,并打开一个{}块。里面有点像match语句。

这里有一个只取(),然后返回6:

macro_rules! give_six {
    () => {
        6
    };
}

fn main() {
    let six = give_six!();
    println!("{}", six);
}

但这和match语句是不一样的,因为宏实际上不会编译任何东西。它只是接受一个输入并给出一个输出。然后编译器会检查它是否有意义。这就是为什么宏就像 "写代码的代码"。你会记得,一个真正的match语句需要给出相同的类型,所以这不会工作:

fn main() {
// ⚠️
    let my_number = 10;
    match my_number {
        10 => println!("You got a ten"),
        _ => 10,
    }
}

它会抱怨你在一种情况下要返回(),在另一种情况下要返回i32

error[E0308]: `match` arms have incompatible types
 --> src\main.rs:5:14
  |
3 | /     match my_number {
4 | |         10 => println!("You got a ten"),
  | |               ------------------------- this is found to be of type `()`
5 | |         _ => 10,
  | |              ^^ expected `()`, found integer
6 | |     }
  | |_____- `match` arms have incompatible types

但宏并不关心,因为它只是给出一个输出。它不是一个编译器--它是代码前的代码。所以你可以这样做:

macro_rules! six_or_print {
    (6) => {
        6
    };
    () => {
        println!("You didn't give me 6.");
    };
}

fn main() {
    let my_number = six_or_print!(6);
    six_or_print!();
}

这个就好办了,打印的是You didn't give me 6.。你也可以看到,这不是匹配分支,因为没有_行。我们只能给它(6),或者(),其他的都会出错。而我们给它的6甚至不是i32,只是一个输入6。其实你可以设置任何东西作为宏的输入,因为它只是看输入,看得到什么。比如说:

macro_rules! might_print {
    (THis is strange input 하하はは哈哈 but it still works) => {
        println!("You guessed the secret message!")
    };
    () => {
        println!("You didn't guess it");
    };
}

fn main() {
    might_print!(THis is strange input 하하はは哈哈 but it still works);
    might_print!();
}

所以这个奇怪的宏只响应两件事。()(THis is strange input 하하はは哈哈 but it still works). 没有其他的东西。它打印的是:

You guessed the secret message!
You didn't guess it

所以宏不完全是Rust语法。但是宏也可以理解你给它的不同类型的输入。拿这个例子来说。

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {}", $input);
    }
}

fn main() {
    might_print!(6);
}

这将打印You gave me: 6$input:expr部分很重要。它的意思是 "对于一个表达式,给它起一个变量名$input"。在宏中,变量以$开头。在这个宏中,如果你给它一个表达式,它就会打印出来。我们再来试一试。

macro_rules! might_print {
    ($input:expr) => {
        println!("You gave me: {:?}", $input); // Now we'll use {:?} because we will give it different kinds of expressions
    }
}

fn main() {
    might_print!(()); // give it a ()
    might_print!(6); // give it a 6
    might_print!(vec![8, 9, 7, 10]); // give it a vec
}

这将打印:

You gave me: ()
You gave me: 6
You gave me: [8, 9, 7, 10]

另外注意,我们写了{:?},但它不会检查&input是否实现了Debug。它只会写代码,并尝试让它编译,如果没有,那么它就会给出一个错误。

那么除了expr,宏还能看到什么呢?它们是 block | expr | ident | item | lifetime | literal | meta | pat | path | stmt | tt | ty | vis. 这就是复杂的部分。你可以在这里看到它们各自的意思,这里说:

item: an Item
block: a BlockExpression
stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
pat: a Pattern
expr: an Expression
ty: a Type
ident: an IDENTIFIER_OR_KEYWORD
path: a TypePath style path
tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
meta: an Attr, the contents of an attribute
lifetime: a LIFETIME_TOKEN
vis: a possibly empty Visibility qualifier
literal: matches -?LiteralExpression

另外有一个很好的网站叫cheats.rs,在这里解释了它们,并且每个都给出了例子。

然而,对于大多数宏,你只会用到 expridentttident 表示标识符,用于变量或函数名称。tt表示token树,和任何类型的输入。让我们尝试用这两个词创建一个简单的宏。

macro_rules! check {
    ($input1:ident, $input2:expr) => {
        println!(
            "Is {:?} equal to {:?}? {:?}",
            $input1,
            $input2,
            $input1 == $input2
        );
    };
}

fn main() {
    let x = 6;
    let my_vec = vec![7, 8, 9];
    check!(x, 6);
    check!(my_vec, vec![7, 8, 9]);
    check!(x, 10);
}

所以这将取一个ident(像一个变量名)和一个表达式,看看它们是否相同。它的打印结果是

Is 6 equal to 6? true
Is [7, 8, 9] equal to [7, 8, 9]? true
Is 6 equal to 10? false

而这里有一个宏,输入tt,然后把它打印出来。它先用一个叫stringify!的宏创建一个字符串。

macro_rules! print_anything {
    ($input:tt) => {
        let output = stringify!($input);
        println!("{}", output);
    };
}

fn main() {
    print_anything!(ththdoetd);
    print_anything!(87575oehq75onth);
}

这个将打印:

ththdoetd
87575oehq75onth

但是如果我们给它一些带有空格、逗号等的东西,它就不会打印。它会认为我们给了它不止一个元素或额外的信息,所以它会感到困惑。

这就是宏开始变得困难的地方。

要一次给宏提供多个元素,我们必须使用不同的语法。不要用$input,而是$($input1),*。这意味着零或更多(这是 * 的意思),用逗号分隔。如果你想要一个或多个,请使用 + 而不是 *

现在我们的宏看起来是这样的。

macro_rules! print_anything {
    ($($input1:tt),*) => {
        let output = stringify!($($input1),*);
        println!("{}", output);
    };
}


fn main() {
    print_anything!(ththdoetd, rcofe);
    print_anything!();
    print_anything!(87575oehq75onth, ntohe, 987987o, 097);
}

所以它接受任何用逗号隔开的token树,并使用 stringify! 把它变成一个字符串。然后打印出来。它的打印结果是:

ththdoetd, rcofe

87575oehq75onth, ntohe, 987987o, 097

如果我们使用+而不是*,它会给出一个错误,因为有一次我们没有给它输入。所以*是一个比较安全的选择。

所以现在我们可以开始看到宏的威力了。在接下来的这个例子中,我们实际上可以创建我们自己的函数:

macro_rules! make_a_function {
    ($name:ident, $($input:tt),*) => { // First you give it one name for the function, then it checks everything else
        fn $name() {
            let output = stringify!($($input),*); // It makes everything else into a string
            println!("{}", output);
        }
    };
}


fn main() {
    make_a_function!(print_it, 5, 5, 6, I); // We want a function called print_it() that prints everything else we give it
    print_it();
    make_a_function!(say_its_nice, this, is, really, nice); // Same here but we change the function name
    say_its_nice();
}

这个打印:

5, 5, 6, I
this, is, really, nice

所以现在我们可以开始了解其他的宏了。你可以看到,我们已经使用的一些宏非常简单。这里是我们用来写入文件的write!的那个:


#![allow(unused)]
fn main() {
macro_rules! write {
    ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*)))
}
}

要使用它,你就输入这个:

  • 一个表达式(expr) 得到变量名$dst.
  • 之后的一切。如果它写的是$arg:tt,那么它只会取1个,但是因为它写的是$($arg:tt)*,所以它取0,1,或者任意多个。

然后它取$dst,并对它使用了一个叫做write_fmt的方法。在这里面,它使用了另一个叫做format_args!的宏,它接受所有的$($arg)*,或者我们输入的所有参数。

现在我们来看一下todo!这个宏。当你想让程序编译但还没有写出你的代码时,就会用到这个宏。它看起来像这样:


#![allow(unused)]
fn main() {
macro_rules! todo {
    () => (panic!("not yet implemented"));
    ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+)));
}
}

这个有两个选项:你可以输入(),也可以输入一些token树(tt)。

  • 如果你输入(),它只是panic!,并加上一个信息。所以其实你可以直接写panic!("not yet implemented"),而不是todo!,这也是一样的。
  • 如果你输入一些参数,它会尝试打印它们。你可以看到里面有同样的format_args!宏,它的工作原理和println!一样。

所以,如果你写了这个,它也会工作:

fn not_done() {
    let time = 8;
    let reason = "lack of time";
    todo!("Not done yet because of {}. Check back in {} hours", reason, time);
}

fn main() {
    not_done();
}

这将打印:

thread 'main' panicked at 'not yet implemented: Not done yet because of lack of time. Check back in 8 hours', src/main.rs:4:5

在一个宏里面,你甚至可以调用同一个宏。这里有一个。

macro_rules! my_macro {
    () => {
        println!("Let's print this.");
    };
    ($input:expr) => {
        my_macro!();
    };
    ($($input:expr),*) => {
        my_macro!();
    }
}

fn main() {
    my_macro!(vec![8, 9, 0]);
    my_macro!(toheteh);
    my_macro!(8, 7, 0, 10);
    my_macro!();
}

这个可以取(),也可以取一个表达式,也可以取很多表达式。但是不管你放什么表达式,它都会忽略所有的表达式,只是在()上调用my_macro!。所以输出的只是Let's print this,四次。

dbg!宏中也可以看到同样的情况,也是调用自己。


#![allow(unused)]
fn main() {
macro_rules! dbg {
    () => {
        $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); //$crate means the crate that it's in.
    };
    ($val:expr) => {
        // Use of `match` here is intentional because it affects the lifetimes
        // of temporaries - https://stackoverflow.com/a/48732525/1063961
        match $val {
            tmp => {
                $crate::eprintln!("[{}:{}] {} = {:#?}",
                    $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);
                tmp
            }
        }
    };
    // Trailing comma with single argument is ignored
    ($val:expr,) => { $crate::dbg!($val) };
    ($($val:expr),+ $(,)?) => {
        ($($crate::dbg!($val)),+,)
    };
}
}

(eprintln!println!相同,只打印到io::stderr而不是io::stdout。还有eprint!不增加一行)。)

所以我们可以自己去试一试。

fn main() {
    dbg!();
}

这与第一分支相匹配,所以它会用file!line!宏打印文件名和行名。它打印的是[src/main.rs:2]

我们用这个来试试。

fn main() {
    dbg!(vec![8, 9, 10]);
}

这将匹配下一个分支,因为它是一个表达式。然后它将调用输入tmp并使用这个代码。 $crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);. 所以它会用file!line!来打印,然后把$val做成String,用{:#?}来漂亮的打印tmp。所以对于我们的输入,它会这样写。

[src/main.rs:2] vec![8, 9, 10] = [
    8,
    9,
    10,
]

剩下的部分,即使你多加了一个逗号,它也只是自己调用dbg!

正如你所看到的,宏是非常复杂的!通常你只想让一个宏自动完成一些简单函数不能很好完成的事情。学习宏的最好方法是看其他宏的例子。没有多少人能够快速写出宏而不出问题。所以不要认为你需要知道宏的一切,才能知道如何在Rust中写。但如果你读了其他宏,并稍加修改,你就可以很容易地借用它们的力量。然后你可能会开始适应写自己的宏。

第2部分 - 电脑上的Rust

你看到了,我们几乎可以使用Playground学习Rust中的任何东西。但如果你到目前为止已经学了这么多,现在你可能会想要在你的电脑上使用Rust。总有一些事情是你不能用Playground做的,比如使用文件或代码在多个文件中。其他如输入和flags也需要在电脑上安装Rust。但最重要的是,在你的电脑上有了Rust,你可以使用Crate。我们已经了解了crate,但在playground中你只能使用最流行的crate。但在你的电脑上,你可以在程序中使用任何crate。