防 rust 常量可迭代关联数组

c0vxltue  于 2023-01-17  发布在  其他
关注(0)|答案(1)|浏览(140)

我想创建一个类似于编译时不可变Map的结构,在编译时安全地检查键,更一般地说,我想创建一个具有安全键访问的可迭代关联数组。
我的第一次尝试是使用const HashMap(如所描述的here),但密钥无法安全访问:

use phf::{phf_map};

static COUNTRIES: phf::Map<&'static str, &'static str> = phf_map! {
    "US" => "United States",
    "UK" => "United Kingdom",
};

COUNTRIES.get("EU") // no compile-time error

我考虑的另一个选项是对strum crate使用可枚举枚举枚举,如here所述:

use strum::IntoEnumIterator; // 0.17.1
use strum_macros::EnumIter; // 0.17.1

#[derive(Debug, EnumIter)]
enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST,
}

fn main() {
    for direction in Direction::iter() {
        println!("{:?}", direction);
    }
}

除了rust中的enum值只能是整数之外,这是可以实现的。要分配一个不同的值,需要类似于用match语句实现枚举的value()函数。(例如所描述的here),然而这意味着每当开发人员决定附加新项时,value函数也必须被更新,并且每次在两个地方重写枚举名并不理想。
我最后一次尝试是在impl中使用const s,如下所示:

struct MyType {
    value: &'static str
}

impl MyType {
    const ONE: MyType = MyType { value: "one" };
    const TWO: MyType = MyType { value: "two" };
}

这允许单写实现,并且对象是安全可访问的编译时常量,但是我发现没有办法迭代它们(如变通方法here所示)(尽管这可能是某种过程宏的可能)。
我来自很多TypeScript,在那里这种任务非常简单:

const values = {
  one: "one",
  two: "two" // easy property addition
}

values.three; // COMPILE-TIME error
Object.keys(values).forEach(key => {...}) // iteration

或者甚至在Java中,这可以简单地通过带有属性的enums来完成。
我知道这听起来有点像XY问题,但我并不认为这是一件荒谬的事情,通常要求一个 * 安全,可迭代,编译时不可变常量关联数组 *(天哪,这是不是有点拗口)。这种模式在Rust中可能吗?事实上,我在上面找不到任何东西,而且它看起来很难,这让我相信我所做的不是Rust代码的最佳实践。在这种情况下,有什么替代方案呢?如果这对Rust来说是一个糟糕的设计模式,那么什么会是一个好的替代方案呢?

gcxthw6b

gcxthw6b1#

@JakubDóka我该如何实现它?我看了一些过程宏,但似乎不明白如何实现这样的宏。

macro_rules! decl_named_iterable_enum {
    (
        // notice how we format input as it should be inputted (good practice)

        // here is the indentifier bound to $name variable, when we later mention it
        // it will be replaced with the passed value
        $name:ident {
            // the `$(...)*` matches 0-infinity of consecutive `...`
            // the `$(...)?` matches 0-1 of `...`
            $($variant:ident $(= $repr:literal)?,)*
        }
    ) => {
        #[derive(Clone, Copy)]
        enum $name {
            // We use the metavar same way we bind it,
            // just ommitting its token type
            $($variant),*
            //         ^ this will insert `,` between the variants
        }

        impl $name {
            // same story just with additional tokens
            pub const VARIANTS: &[Self] = &[$(Self::$variant),*];

            pub const fn name(self) -> &'static str {
                match self {
                    $(
                        // see comments on other macro branches, this si a
                        // common way to handle optional patterns
                        Self::$variant => decl_named_iterable_enum!(@repr $variant $($repr)?),
                    )*
                }
            }
        }
    };

    // this branch will match if literal is present
    // in this case we just ignore the name
    (@repr $name:ident $repr:literal) => {
        $repr
    };

    // fallback for no literal provided,
    // we stringify the name of variant
    (@repr $name:ident) => {
        stringify!($name)
    };
}

// this is how you use the macro, similar to typescript
decl_named_iterable_enum! {
    MyEnum {
        Variant,
        Short = "Long",
    }
}

// some example code collecting names of variants
fn main() {
    let name_list = MyEnum::VARIANTS
        .iter()
        .map(|v| v.name())
        .collect::<Vec<_>>();

    println!("{name_list:?}");
}

// Exercise for you:
//    1. replace `=` for name override with `:`
//    2. add a static `&[&str]` names accessed by `MyEnum::VARIANT_NAMES`

相关问题