简体   繁体   English

是否可以使用 Rust 宏程序性地声明变量?

[英]Is it possible to declare variables procedurally using Rust macros?

Basically, there are two parts to this question:基本上,这个问题有两个部分:

  1. Can you pass an unknown identifier to a macro in Rust ?你能将未知标识符传递给Rust 中的宏吗?

  2. Can you combine strings to generate new variable names in a Rust macro?你能在 Rust 宏中组合字符串来生成新的变量名吗?

For example, something like:例如,类似于:

macro_rules! expand(
  ($x:ident) => (
    let mut x_$x = 0;
  )
)

Calling expand!(hi) obvious fails because hi is an unknown identifier;调用 expand!(hi) 显然失败,因为 hi 是一个未知标识符; but can you somehow do this?但你能以某种方式做到这一点吗?

ie. IE。 The equivalent in C of something like:在 C 中的等价物如下:

#include <stdio.h>
#define FN(Name, base) \
  int x1_##Name = 0 + base; \
  int x2_##Name = 2 + base; \
  int x3_##Name = 4 + base; \
  int x4_##Name = 8 + base; \
  int x5_##Name = 16 + base;

int main() {
  FN(hello, 10)
  printf("%d %d %d %d %d\n", x1_hello, x2_hello, x3_hello, x4_hello, x5_hello);
  return 0;
}

Why you say, what a terrible idea.你为什么这么说,多么可怕的想法。 Why would you ever want to do that?你为什么要这样做?

I'm glad you asked!我很高兴你问了!

Consider this rust block:考虑这个锈块:

{
   let marker = 0;
   let borrowed = borrow_with_block_lifetime(data, &marker); 
   unsafe {
      perform_ffi_call(borrowed);
   }
}

You now have a borrowed value with an explicitly bounded lifetime (marker) that isn't using a structure lifetime, but that we can guarantee exists for the entire scope of the ffi call;您现在有一个具有明确限定的生命周期(标记)的借用值,该值不使用结构生命周期,但我们可以保证在 ffi 调用的整个范围内都存在; at the same time we don't run into obscure errors where a * is de-referenced unsafely inside an unsafe block and so the compiler doesn't catch it as an error, despite the error being made inside a safe block .同时,我们不会遇到模糊的错误,其中*在不安全块内被不安全地取消引用,因此编译器不会将其捕获为错误,尽管错误是在安全块内发生的

(see also Why are all my pointers pointing to the same place with to_c_str() in rust? ) (另请参阅为什么我的所有指针都指向与 rust 中的 to_c_str() 相同的位置?

The use a macro that can declare temporary variables for this purpose would considerably ease the troubles I have fighting with the compiler.使用可以为此目的声明临时变量的宏将大大减轻我与编译器斗争的麻烦。 That's why I want to do this.这就是为什么我想这样做。

Yes however this is only available as a nightly-only experimental API which may be removed.是的,但是这仅作为夜间专用的实验性 API 可用,可能会被删除。

You can pass arbitrary identifier into a macro and yes, you can concatenate identifiers into a new identifier using concat_idents!() macro:您可以将任意标识符传递给宏,是的,您可以使用concat_idents!()宏将标识符连接到新标识符中:

#![feature(concat_idents)]

macro_rules! test {
    ($x:ident) => ({
        let z = concat_idents!(hello_, $x);
        z();
    })
}

fn hello_world() {  }

fn main() {
    test!(world);
}

However, as far as I know, because concat_idents!() itself is a macro, you can't use this concatenated identifier everywhere you could use plain identifier, only in certain places like in example above, and this, in my opinion, is a HUGE drawback.然而,据我所知,因为concat_idents!()本身是一个宏,你不能在任何可以使用普通标识符的地方使用这个连接标识符,只能在上面例子中的某些地方使用,在我看来,这是一个巨大的缺点。 Just yesterday I tried to write a macro which could remove a lot of boilerplate in my code, but eventually I was not able to do it because macros do not support arbitrary placement of concatenated identifiers.就在昨天,我试图编写一个宏,它可以删除我代码中的很多样板,但最终我无法做到,因为宏不支持任意放置连接标识符。

BTW, if I understand your idea correctly, you don't really need concatenating identifiers to obtain unique names.顺便说一句,如果我正确理解你的想法,你真的不需要连接标识符来获得唯一的名称。 Rust macros, contrary to the C ones, are hygienic .与 C 宏相反,Rust 宏是卫生的 This means that all names of local variables introduced inside a macro won't leak to the scope where this macro is called.这意味着在宏中引入的所有局部变量的名称都不会泄漏到调用此宏的范围内。 For example, you could assume that this code would work:例如,您可以假设此代码有效:

macro_rules! test {
    ($body:expr) => ({ let x = 10; $body })
}

fn main() {
    let y = test!(x + 10);
    println!("{}", y);
}

That is, we create a variable x and put an expression after its declaration.也就是说,我们创建了一个变量x并在其声明后放置了一个表达式。 It is then natural to think that x in test!(x + 10) refers to that variable declared by the macro, and everything should be fine, but in fact this code won't compile:然后很自然地认为x in test!(x + 10)指的是宏声明的那个变量,一切都应该没问题,但实际上这段代码不会编译:

main3.rs:8:19: 8:20 error: unresolved name `x`.
main3.rs:8     let y = test!(x + 10);
                             ^
main3.rs:3:1: 5:2 note: in expansion of test!
main3.rs:8:13: 8:27 note: expansion site
error: aborting due to previous error

So if all you need is uniqueness of locals, then you can safely do nothing and use any names you want, they will be unique automatically.因此,如果您只需要本地人的唯一性,那么您可以安全地什么都不做并使用您想要的任何名称,它们将自动唯一。 This is explained in macro tutorial, though I find the example there somewhat confusing.这在宏教程中进行了解释,尽管我发现那里的示例有些令人困惑。

In cases where concat_idents doesn't work (which is most cases I'd like to use it) changing the problem from concatenated identifiers to using namespaces does work.concat_idents不起作用的情况下(大多数情况下我想使用它)将问题从连接标识符更改为使用名称空间确实有效。

That is, instead of the non-working code:也就是说,而不是非工作代码:

macro_rules! test {
    ($x:ident) => ({
        struct concat_idents!(hello_, $x) {}
        enum contact_idents!(hello_, $x) {}
    })
}

The user can name the namespace, and then have preset names as shown below:用户可以命名命名空间,然后预设名称如下:

macro_rules! test {
    ($x:ident) => ({
        mod $x {
            struct HelloStruct {}
            enum HelloEnum {}
        }
    })
}

Now you have a name based on the macro's argument.现在您有了一个基于宏参数的名称。 This technique is only helpful in specific cases.此技术仅在特定情况下有用。

There is also https://github.com/dtolnay/paste , which works well in casese where concat_idents is underpowered or in cases where you can't target the nightly compiler.还有https://github.com/dtolnay/paste ,它在concat_idents功能不足或无法定位夜间编译器的情况下效果很好。

macro_rules! foo_macro {
    ( $( $name:ident ),+ ) => {
        paste::item! {
            #[test]
            fn [<test_ $name>]() {
                assert! false
            }
        }
    };
}

You can collect your identifiers into a struct if you don't want to use nightly and external crates and your identifiers are types.如果您不想使用夜间和外部 crate 并且您的标识符是类型,则可以将标识符收集到一个结构中。

use std::fmt::Debug;

fn print_f<T: Debug>(v: &T){
    println!("{:?}", v);
}

macro_rules! print_all {
    ($($name:ident),+) => {
        struct Values{
            $($name: $name),+
        }
        let values = Values{
            $(
                $name: $name::default()
            ),+
        };
        $(
            print_f(&values.$name);
        )+
    };
}

fn main(){
    print_all!(String, i32, usize);
}

This code prints此代码打印

""
0
0

If you fear that Value will conflict with some type name, you can use some long UUID as part of the name:如果您担心Value会与某些类型名称冲突,您可以使用一些长 UUID 作为名称的一部分:

struct Values_110cf51d7a694c808e6fe79bf1485d5b{
   $($name:$name),+
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM