如何根据 Rust 中的字符串选择结构体?

RBF*_*F06 4 string dynamic-dispatch rust trait-objects

问题陈述

\n

我有一组结构 、ABCD,它们都实现了一个 Trait\n Runnable

\n
trait Runnable {\n    fn run(&mut self);\n}\nimpl Runnable for A {...}\nimpl Runnable for B {...}\nimpl Runnable for C {...}\nimpl Runnable for D {...}\n
Run Code Online (Sandbox Code Playgroud)\n

我还有一个结构体,用作构造、\n 、和实例Config的规范。ABCD

\n
struct Config {\n    filename: String,\n    other_stuff: u8,\n}\n\nimpl From<Config> for A {...}\nimpl From<Config> for B {...}\nimpl From<Config> for C {...}\nimpl From<Config> for D {...}\n
Run Code Online (Sandbox Code Playgroud)\n

在我的程序中,我想解析一个Config实例并根据字段的值构造A,\n B, C, 或,然后对其进行调用\n 。应通过根据字符串顺序检查每个结构并选择第一个“匹配”该字符串的结构来选择结构。DfilenameRunnable::runfilename

\n

Na\xc3\xafve 实现

\n

这是一个 na\xc3\xafve 实现。

\n
trait CheckFilename {\n    fn check_filename(filename: &str) -> bool;\n}\nimpl CheckFilename for A {...}\nimpl CheckFilename for B {...}\nimpl CheckFilename for C {...}\nimpl CheckFilename for D {...}\n\n\nfn main() {\n    let cfg: Config = get_config(); // Some abstract way of evaluating a Config at runtime.\n\n    let mut job: Box<dyn Runnable> = if A::check_filename(&cfg.filename) {\n        println!("Found matching filename for A");\n        Box::new(A::from(cfg))\n    } else if B::check_filename(&cfg.filename) {\n        println!("Found matching filename for B");\n        Box::new(B::from(cfg))\n    } else if C::check_filename(&cfg.filename) {\n        println!("Found matching filename for C");\n        Box::new(C::from(cfg))\n    } else if D::check_filename(&cfg.filename) {\n        println!("Found matching filename for D");\n        Box::new(D::from(cfg))\n    } else {\n        panic!("did not find matching pattern for filename {}", cfg.filename);\n    };\n\n    job.run();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这可行,但有一些代码味道:

\n
    \n
  • 在我看来,巨大的if else if else if else if else...声明很臭
  • \n
  • 大量重复:用于检查文件名、打印选择的结构类型以及从配置构造实例的代码对于每个分支都是相同的,并且仅在它们正在处理的结构类型方面有所不同。有没有办法抽象掉这种重复?
  • \n
  • 非常容易出错:由于未能将结构与谓词同步,很容易意外地搞砸文件名\n字符串和结构之间的映射;例如\n写一些类似的内容:\n
    if D::check_filename(&cfg.filename) {\n    println!("Found matching filename for D");\n    Box::new(B::from(cfg)) // Developer error: constructs a B instead of a D.\n}\n
    Run Code Online (Sandbox Code Playgroud)\n编译器不会捕获这一点。
  • \n
  • 向程序中添加新结构(例如、E、等)不太符合人体工程学。它需要为主语句中的每个语句添加一个新分支。简单地将结构添加到某种结构类型的“主列表”中会更好。FGif else
  • \n
\n
\n

是否有更优雅或更惯用的方法来解决这些气味?

\n

cdh*_*wie 8

由于转换会消耗Config,统一所有类型逻辑的挑战是您需要有条件地移动配置值才能进行转换。标准库有多种可能出错的消费函数的情况,它们使用的模式是 return Result,返还该Err情况下可能消费的值。例如,Arc::try_unwrap提取 an 的内部值Arc,但如果失败,则会ArcErr变体中给出后面的值。

我们可以在这里做同样的事情,创建一个函数,如果文件名匹配,它会生成适当的结构之一,但在出现错误时返回配置:

fn try_convert_config_to<T>(config: Config) -> Result<Box<dyn Runnable>, Config>
where
    T: Runnable + CheckFilename + 'static,
    Config: Into<T>,
{
    if T::check_filename(&config.filename) {
        Ok(Box::new(config.into()))
    } else {
        Err(config)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以编写另一个函数,其中包含该函数的特定实例化的静态切片,并且它可以按顺序尝试每个函数,直到成功为止。由于我们将配置移动到每个加载器函数中,因此我们必须将其放回容器中,Err以便下一个循环迭代可以再次移动它。

fn try_convert_config(mut config: Config) -> Option<Box<dyn Runnable>> {
    static CONFIG_LOADERS: &[fn(Config) -> Result<Box<dyn Runnable>, Config>] = &[
        try_convert_config_to::<A>,
        try_convert_config_to::<B>,
        try_convert_config_to::<C>,
        try_convert_config_to::<D>,
    ];

    for loader in CONFIG_LOADERS {
        match loader(config) {
            Ok(c) => return Some(c),
            Err(c) => config = c,
        };
    }

    None
}
Run Code Online (Sandbox Code Playgroud)

这解决了您所有的担忧:

  • 不再有巨大的 if-else 链,只有一个循环。
  • 代码重复消失了,因为try_convert_config_to一次性实现了所有类型的逻辑。
  • 只要您check_filename使用.intotry_convert_config_to
  • 要添加新类型,您只需向CONFIG_LOADERS切片添加新元素即可。

游乐场