将嵌套结构字段路径作为宏参数传递

Passing nested struct field path as macro parameter

我是 Rust 的新手,我无法编译以下代码:

#![feature(trace_macros)]

fn main() {
    #[derive(Debug)]
    struct Inner {
      value: u8
    }
    
    #[derive(Debug)]
    struct Outer {
      inner: Inner
    }
    
    let mut x  = Outer { inner: Inner { value: 64 } };
    
    /********/
    
    macro_rules! my_macro {
        ($field_path:expr, $v:expr) => {
            x.$field_path = $v;
        }
    }

    trace_macros!(true);    
    // my_macro!(inner, Inner { value: 42 }); // only works with $field_path:ident
    my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
    trace_macros!(false);
    
    x . inner.value = 42; // works fine
    
    assert_eq!(42, x.inner.value);
}

我收到以下错误:

error: unexpected token: `inner.value`
  --> src/main.rs:20:15
   |
20 |             x.$field_path = $v;
   |               ^^^^^^^^^^^
...
26 |     my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
   |     --------------------------- in this macro invocation
   |

...

error: expected one of `.`, `;`, `?`, `}`, or an operator, found `inner.value`
  --> src/main.rs:20:15
   |
20 |             x.$field_path = $v;
   |               ^^^^^^^^^^^ expected one of `.`, `;`, `?`, `}`, or an operator
...
26 |     my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
   |     --------------------------- in this macro invocation
   |

...

不过trace_macro好像可以展开my_macro!:

note: trace_macro
  --> src/main.rs:26:5
   |
26 |     my_macro!(inner.value, 42); // expected output: x.inner.value = 42;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: expanding `my_macro! { inner . value, 42 }`
   = note: to `x . inner.value = 42 ;` <<< exactly what I am looking for

如果我将 $field_path 参数保留为 ident,我只是得到 no rules expected the token `.` ,我认为这是有道理的,因为 . 是一个运算符。 我错过了什么?

Playground link

I think the issue lies in the fact that inner.value contains the dot operator.

这正是你的问题。没有一个 fragment specifiers that allows you to match ident and/or a field access expression. The issue with using expr (expressions) 是在扩展时它基本上包含在括号中,即 x.(inner.value),这没有意义,因此你得到 "unexpected token `inner.value`" 的原因。

不过,你确实可以使用ident,你只需要使用repetition即可。

简而言之,您可以 $( $field_path:ident ).+ 而不是 $field_path:ident。然后扩展它,而不是 x.$field_path 然后你做 x. $( $field_path ).+.

macro_rules! my_macro {
    ($($field_path:ident).+, $v:expr) => {
        x.$($field_path).+ = $v;
    };
}

// Now both are allowed
my_macro!(inner.value, 42);
my_macro!(inner, Inner { value: 64 });

它不起作用的原因是 Rust 宏在 AST 节点而不是标记上运行。

用于访问嵌套字段的 AST 如下所示(语法非常松散,其中括号表示 AST 节点):(((x).outer).inner)

您已将 $field_path 设置为 expr,因此它会像这样解析:((inner).outer)。这是访问 inner 变量的 outer 字段的有效表达式。

然后,评估宏会产生如下内容:((x).((inner).outer)),这没有意义。

现在,如何解决?

关键是变量名(如本例中的 x)需要在访问该字段的表达式的 AST“深处”结束。这使得无法通过任何方式将路径作为宏参数“传入”。

你可以做的是传入一个构建路径表达式的宏:

macro_rules! my_macro {
    ($path:ident, $v:expr) => {
        $path!(x) = $v;
    }
}
macro_rules! inner_value {
    ($x:ident) => {
        $x.inner.value
    }
}

my_macro!(inner_value, 42); // expected output: x.inner.value = 42;

(playground)

不过,为了更好的可读性,我会考虑在没有宏的情况下执行此操作。根据您的操作,使用闭包可能有效:

fn do_things(x: &mut Outer, path: impl Fn(&mut Outer) -> &mut u8){
    *(path(x)) = 42;
}

do_things(&mut x, |x| &mut x.inner.value);

(playground)