如何在Rust中基于字符串选择结构体?

x4shl7ld  于 2023-10-20  发布在  其他
关注(0)|答案(2)|浏览(122)

问题陈述

我有一组结构体ABCD,它们都实现了一个trait Runnable

trait Runnable {
    fn run(&mut self);
}
impl Runnable for A {...}
impl Runnable for B {...}
impl Runnable for C {...}
impl Runnable for D {...}

我还有一个结构体Config,它用作构造ABCD示例的规范。

struct Config {
    filename: String,
    other_stuff: u8,
}

impl From<Config> for A {...}
impl From<Config> for B {...}
impl From<Config> for C {...}
impl From<Config> for D {...}

在我的程序中,我想解析一个Config示例,并根据filename字段的值构造一个ABCD,然后在其上调用Runnable::run。结构体的选择方法是根据filename字符串顺序检查每个结构体,然后选择第一个与该字符串“匹配”的结构体。

简单实现

下面是一个简单的实现。

trait CheckFilename {
    fn check_filename(filename: &str) -> bool;
}
impl CheckFilename for A {...}
impl CheckFilename for B {...}
impl CheckFilename for C {...}
impl CheckFilename for D {...}

fn main() {
    let cfg: Config = get_config(); // Some abstract way of evaluating a Config at runtime.

    let mut job: Box<dyn Runnable> = if A::check_filename(&cfg.filename) {
        println!("Found matching filename for A");
        Box::new(A::from(cfg))
    } else if B::check_filename(&cfg.filename) {
        println!("Found matching filename for B");
        Box::new(B::from(cfg))
    } else if C::check_filename(&cfg.filename) {
        println!("Found matching filename for C");
        Box::new(C::from(cfg))
    } else if D::check_filename(&cfg.filename) {
        println!("Found matching filename for D");
        Box::new(D::from(cfg))
    } else {
        panic!("did not find matching pattern for filename {}", cfg.filename);
    };

    job.run();
}

这是可行的,但有一些代码气味:

  • 巨人if else if else if else if else...声明是臭海事组织
  • 很多重复:用于检查文件名、打印所选结构类型以及从配置构造示例的代码对于每个分支都是相同的,只是它们处理的结构类型不同。有没有办法把这种重复抽象出来?
  • 非常容易出错:很容易由于不能同步结构和 predicate 而意外地搞砸文件名字符串和结构之间的Map;例如写这样的东西:
if D::check_filename(&cfg.filename) {
    println!("Found matching filename for D");
    Box::new(B::from(cfg)) // Developer error: constructs a B instead of a D.
}

,编译器不会捕获它。

  • 添加新结构(例如EFG等)到程序不是很符合人体工程学。它需要为每个分支添加一个新的分支到主if else语句中。简单地将结构体添加到某种结构体类型的“主列表”中会更好。

有没有更优雅或更习惯的方法来处理这些气味?

3hvapo4f

3hvapo4f1#

由于转换会消耗Config,因此统一所有类型的逻辑的挑战在于,您需要有条件地移动config值以进行转换。标准库有多种易出错的消费函数的情况,它们使用的模式是返回Result,在Err情况下返回可能被消费的值。例如,Arc::try_unwrap提取Arc的内部值,但如果这失败了,它会在Err变体中返回Arc
我们可以在这里做同样的事情,创建一个单一的函数,如果文件名匹配,它会产生一个适当的结构体,但在错误时返回配置:

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)
    }
}

然后,您可以编写另一个函数,其中包含此函数的特定示例化的静态切片,并且它可以按顺序尝试每个示例化,直到其中一个成功为止。由于我们将配置移动到每个加载器函数中,因此我们必须将其放回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
}

这解决了您的所有问题:

  • 不再有一个巨大的if-else链,只是一个循环。
  • 代码重复消失了,因为try_convert_config_to一次实现了所有类型的逻辑。
  • 只要您使用try_convert_config_to,就不可能在不同类型上意外调用进程的两个部分(check_filenameinto)。
  • 要添加一个新类型,你只需要在CONFIG_LOADERS切片中添加一个新元素。

Playground

h5qlskok

h5qlskok2#

提出我自己的问题来清理一些被接受的答案留下的小问题,并提供一些额外的解释和参考。向@cdhowie大喊,因为它提供了我解决问题所需的公认答案和知识。

概述

通过重构一些东西,就有可能解决朴素实现的所有问题。技巧是创建一个简单的易错转换函数,如果文件名匹配,则将Config值转换为Box<dyn Runnable>,但如果文件名不匹配,则将Config值返回到Err变量中,以便下一个专门化可以尝试。

定义需求

首先,让我们列出每种类型必须满足的最低要求,以支持我们的应用程序:
1.必须是Runnable,因为我们打算run()它一旦示例化。
1.必须是From<Config>,因为我们打算从Config创建一个
1.必须有一个名称与它相关联,这样我们就可以打印日志消息,标识选择了哪种类型。
1.必须能够确定它是否“匹配”filename字符串
定义了这些需求后,我们定义了一个Trait来执行它们:

trait Job: Runnable + From<Config> {
    const NAME: &'static str;
    fn check_filename(filename: &str) -> bool;
}

需求1和需求2使用超级特征编码,需求3使用相关常量编码
注意:在问题陈述中,这个trait被命名为CheckFilename,但在这里它被重命名为Job,因为考虑到额外的需求,这似乎更合适。
为每个结构体实现Job非常简单:

impl Job for A {
    // This str will be the used as the name of the struct when printing the log message
    const NAME: &'static str = "A";
    fn check_filename(filename: &str) -> bool {
        // logic determining if a `str` matches a Job type goes here
        filename.starts_with('A')
    }
}
impl Job for B {...}
impl Job for C {...}
impl Job for D {...}

创建通用转换函数

接下来,我们创建一个简单的易错函数,它尝试将Config值转换为Box<dyn Runnable>,但如果不匹配,则返回Config值(带所有权)。此函数对所有Job类型通用。

fn try_convert_config_to<T: Job + 'static>(cfg: Config) -> Result<Box<dyn Runnable>, Config> {
    if T::check_filename(&cfg.filename) {
        println!("Found matching job for filename `{}`: {}", cfg.filename, T::NAME);
        Ok(Box::<T>::new(cfg.into()))
    } else {
        Err(cfg)
    }
}

把它连在一起

最后,一个(盒装的)Runnabletrait object可以通过调用try_convert_config_to::<A>来初始化,并通过一个Result::or_else链顺序传递错误值,剩余的专门化为Job

fn main() {
    let cfg: Config = get_config();
    let mut job: Box<dyn Runnable> = try_convert_config_to::<A>(cfg)
        .or_else(try_convert_config_to::<B>)
        .or_else(try_convert_config_to::<C>)
        .or_else(try_convert_config_to::<D>)
        .map_err(|cfg| panic!("Couldn't find matching type for filename `{}`", cfg.filename))
        .unwrap();

    job.run();
}

Playground

相关问题