使用宏输出作为方法名

Using macro output as a method name

我正在尝试编写一个自动将一组枚举变体扩展为构建器的宏(基于 ,但我认为这不相关)。基本上,我想向它传递一个值、一个构建器结构和一个枚举变体列表并生成匹配武器。结果应该等同于:

match a {
    Attribute::InsuranceGroup { value } => builder.insurance_group(value),
};

我想我已经相当接近了,但我不知道如何使用 [=26= 将 UpperCamelCase InsuranceGroup 转换为 lower_camel_case insurance_group ] 板条箱,以便调用构建器方法。目前,我有:

macro_rules! unwrap_value {
    ($var:ident, $builder:ident, [ $( $enum:ident :: $variant:ident ),+ $(,)? ]) => {
        match $var {
            $($enum::$variant { value } => $builder.snake!($variant) (value),)*
        }
    }
}

unwrap_value! {
    a, builder, [Attribute::InsuranceGroup]
}

然而,这在 $builder.snake!($variant) 处失败,分析器抱怨它期望 (,.::?, } 或运算符,而不是 !.

我也试过把 snake! 移到外面,snake!($builder.$variant),但是它说在这个范围内找不到 $builder

虽然我对使用可以消除此问题的生成器的替代方案的任何建议感兴趣,但我更感兴趣的是了解我在使用宏时做错了什么,以便更好地理解它们。

经过数小时的搜寻,我终于在发布后不久找到了解决方案。我没有使用 casey crate,而是使用 paste crate,它包含我需要的功能。宏代码则变为:

macro_rules! unwrap_value {
    ($var:ident, $builder:ident, [ $( $enum:ident :: $variant:ident ),+ $(,)? ]) => {
        match $var {
            $($enum::$variant { value } => paste!($builder.[<$variant:snake>](value)) ,)*
        }
    }
}

虽然我会强烈推荐使用 paste crate(总下载量为 23,999,580,而在撰写本文时 casey 的总下载量为 7,611,paste 甚至可以在 playground 上使用!),我将在这里解释为什么 casey 没有为了知识而工作(并提出一个解决方案!但是你不应该使用的)。


第一个版本不起作用,因为不能在点后使用宏。 You can check that out easily:

macro_rules! m {
    () => { m };
}
v.m!();
error: expected one of `(`, `.`, `::`, `;`, `?`, `}`, or an operator, found `!`
 --> src/main.rs:6:4
  |
6 | v.m!();
  |    ^ expected one of 7 possible tokens

将来(可能)他们也不会被允许在这里,因为这会与 postfix macros in the future. This is stated by the reference, too: no MacroInvocation is allowed after the dot in MethodCallExpr, TupleIndexingExpr or FieldExpr.

的可能性发生冲突(并混淆)

自然的解决方案是将整个调用包装在宏中(也在那里插入 (value),否则它会认为您已经编写了 (v.method)(value),这是无效的方法调用) : snake!($builder.$variant(value)).

但是,现在编译器报错了一个奇怪的错误:

error[E0425]: cannot find value `builder` in this scope
  --> src\lib.rs:6:44
   |
6  |               $($enum::$variant { value } => snake!($builder.$variant(value)),)*
   |                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: a unit struct with a similar name exists: `Builder`
...
17 |   struct Builder;
   |   --------------- similarly named unit struct `Builder` defined here
...
26 | /     unwrap_value! {
27 | |         a, builder, [Attribute::InsuranceGroup]
28 | |     }
   | |_____- in this macro invocation
   |
   = note: this error originates in the macro `snake` (in Nightly builds, run with -Z macro-backtrace for more info)

什么?什么??当然 builder 在范围内!我们宣布了!就在宏调用之前(至少是我)!

结果是macro hygiene

snake!() 如何生成其标识符?嗯,let's dive into the code...

Ident::new(&transform(ident.to_string()), Span::call_site())

嗯,我们去the docs of Span::call_site()...

The span of the invocation of the current procedural macro. Identifiers created with this span will be resolved as if they were written directly at the macro call location (call-site hygiene) and other code at the macro call site will be able to refer to them as well.

(强调我的。这里的“宏”指的是调用proc-macro,即本例中的snake!())。

事实上,如果我们自己拼出 builder,就可以重现这个错误。因为 宏看不到 builder。它只能看到它创建的变量。其实这里用call-site卫生是casey的bug。 paste 做正确的事情并使用原始标识符的卫生。

事实上,这只是揭示了我们方法的一个更大的问题:如果变量不是 snake-cased 怎么办?应该是,但如果不是,我们将 snake-case 错误!

但是我们能做什么呢? paste crate 让我们可以适当地控制我们想要更改的标识符,但是 casey 却没有(选择 paste 的另一个原因)!我们可以对抗我们的 proc 宏吗?

我们可以吗??

也许吧。我看到至少一种不需要我们这样做的方法(自己找到它!)。但实际上我确实想和我们proc-macro作对。至少尝试一下。为什么不呢?很好笑。

snake!() doesn't look into groups。这意味着,它将更改 AbC 之类的内容 - 但不会更改 (AbC) 之类的内容。所以我们可以将 $builder 括在括号中...

$($enum::$variant { value } => snake!(($builder).$variant(value)),)*

(无需处理 value,因为它已经是 snake-cased,并且源自我们的宏)。

而且有效!瞧! (你不应该依赖它。这可能是一个错误并且可能会发生变化。