【问题标题】:Is there a way to match the parameters to a Rust macro?有没有办法将参数与 Rust 宏匹配?
【发布时间】:2019-12-30 05:07:20
【问题描述】:

考虑以下代码:

trait Trait {
    fn x(&self) -> u32;
}

struct A {}
impl Trait for A {
    fn x(&self) -> u32 {
        10
    }
}

struct B {}
impl Trait for B {
    fn x(&self) -> u32 {
        20
    }
}

struct C {
    created_time: u64,
}

impl Trait for C {
    fn x(&self) -> u32 {
        30
    }
}

impl C {
    pub fn new() -> C {
        C { created_time: 1000 } // for simplicity
    }
}

macro_rules! create {
    ($type:ident) => {
        match stringify!($type) {
            "C" => Box::new(C::new()) as Box<dyn Trait>,
            _ => Box::new($type {}) as Box<dyn Trait>,
        }
    };
}

fn main() {
    let a: Box<dyn Trait> = create!(A);
    let b: Box<dyn Trait> = create!(B);
    let c: Box<dyn Trait> = create!(C);

    assert_eq!(a.x(), 10);
    assert_eq!(b.x(), 20);
    assert_eq!(c.x(), 30);
}

如果您要求编译器扩展宏,则解析为:

let a: Box<dyn T> =
    match "A" {
        "C" => Box::new(C::new()) as Box<dyn T>,
        _ => Box::new(A{}) as Box<dyn T>,
    };
let b: Box<dyn T> =
    match "B" {
        "C" => Box::new(C::new()) as Box<dyn T>,
        _ => Box::new(B{}) as Box<dyn T>,
    };
let c: Box<dyn T> =
    match "C" {
        "C" => Box::new(C::new()) as Box<dyn T>,
        _ => Box::new(C{}) as Box<dyn T>,
    };

这很好地解释了为什么编译器在尝试编译它时会给出以下错误:

error[E0063]: missing field `created_time` in initializer of `C`
  --> mwe.rs:29:27
   |
29 |             _ => Box::new($type { }) as Box<dyn T>,
   |                           ^^^^^ missing `created_time`
...
37 |     let c: Box<dyn T> = create!(C);
   |                         ---------- in this macro invocation

error: aborting due to previous error

但是,我希望编译器会注意到 match "C" { "C" =&gt; ..., _ =&gt; ... } 的情况并删除第二个子句,因为它无论如何都无法运行。遗憾的是它没有,而是抱怨第二个(不可能的)子句无法编译。

我还尝试将宏中的match 替换为if,如下所示,但无济于事:

macro_rules! create {
    ($type:ident) => {
        if stringify!($type) == "C" {
            Box::new(C::new()) as Box<dyn T>
        } else {
            Box::new($type { }) as Box<dyn T>
        }
    }
}

导致

let c: Box<dyn T> =
    if "C" == "C" { Box::new(C::new()) as Box<dyn T> }
    else { Box::new(C{}) as Box<dyn T> };

match 尝试的错误相同。

希望 Haskell 的保护管道语法能在 Rust 中以某种方式工作,我最后还尝试了以下方法:

macro_rules! create {
    ($type:ident) | (stringify!($type) == "C") => {
        Box::new(C::new()) as Box<dyn T>
    },
    ($type:ident) | (stringify!($type) != "C") => {
        Box::new($type { }) as Box<dyn T>
    },
}

但这给了error: no rules expected the token '|'


这最终让我回到标题中的问题:

有没有办法在宏规则中添加“守卫”来告诉编译器“只有在传递了这个参数时才运行 A,或者在其他东西上运行 B”?

【问题讨论】:

  • 这似乎是XY problem。你想用这一切来完成什么?在我看来,最简单的方法是将 new 方法添加到所有 3 种类型并始终使用它。还有你的声明:“遗憾的是它没有,而是抱怨第二个(不可能的)子句无法编译。”是不正确的。 Rust 无法在编译时确定是否会采用某些分支/分支,并且这些分支中的代码仍然必须在语法上正确即使它们从未被调用
  • @sshashank124 确实;我想避免为不需要它的结构创建 new 函数(因为它没有字段或者它们都是公共的),所以我尝试制作一个辅助宏,但我想那是不可能的.
  • 是的,就我个人而言,我会说一个新函数实际上不会超过 2-3 行,并且在 Rust 中被普遍理解。我不确定用不是每个人都可能熟悉的宏包装器替换它一定会更好。再说一次,只是我个人的看法
  • 您还可以为您的结构派生 Default trait,这将使您受益于更少的代码以及一种无需参数实例化实例的统一方式
  • @mcarton 的回答可能是最有效的一个。然而,这样设置似乎是一个 Rust 反模式。为什么不创建一个宏来根据需要将模板应用于每个类。

标签: rust macros match guard


【解决方案1】:

虽然您的问题看起来确实是一个 X/Y 问题,并且可以使用诸如 Default 之类的特征来更优雅地解决,但可以在某种程度上匹配宏参数。

你的宏可以重写为

macro_rules! create {
    (C) => {
        Box::new(C::new()) as Box<dyn Trait>
    };

    ($type:ident) => {
        Box::new($type {}) as Box<dyn Trait>
    };
}

编译器在第一次成功匹配时停止。

请注意,这有一些限制:如您所料,编译器会对标记进行字面比较,例如以下内容将失败:

type D = C;

let really_just_another_c: Box<dyn Trait> = create!(D);

error[E0063]: missing field `created_time` in initializer of `C`
  --> src/main.rs:41:18
   |
41 |         Box::new($type {}) as Box<dyn Trait>
   |                  ^^^^^ missing `created_time`
...
51 |     let c: Box<dyn Trait> = create!(D);
   |                             ---------- in this macro invocation

【讨论】:

  • 虽然确实不是问题的完美解决方案,但它确实为问题提供了完美的答案。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-14
  • 1970-01-01
  • 1970-01-01
  • 2020-11-30
  • 2020-01-11
相关资源
最近更新 更多