问题陈述

我有一组 struct ,ABCD,它们都实现一个特征 Runnable.

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

我还有一个 struct Config,它用作构造A的规范, BCD个实例.

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实例并构造一个ABCD,取决于filename字段的值,然后调用 Runnable::run在上面.应该通过按顺序判断每个 struct 与filename字符串,并 Select 与该字符串"匹配"的第一个 struct 来 Select struct .

天真的实施

以下是一个天真的实现.

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...声明臭气熏天
  • 大量重复:用于判断文件名、打印 struct 类型的代码 和从配置构造实例对于每个分支都是相同的 唯一不同的是它们处理的是哪种 struct 类型.有没有办法把这个抽象出来? 不停地重复?
  • 非常容易出错:很容易意外地搞错文件名之间的映射 字符串和 struct ,因为未能将 struct 与谓词同步;for 编写类似以下内容的示例:
    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.
    }
    
    而编译器不会捕捉到这一点.
  • 向程序添加新的 struct (例如,EFG等)不是很符合人体工程学.它需要在主if else语句中 for each 语句添加一个新分支.简单地将 struct 添加到某种 struct 类型的"主列表"中会好得多.

有没有更优雅或更惯用的方式来解决这些气味?

推荐答案

由于转换使用Config,统一所有类型的逻辑所面临的挑战是,您需要有条件地移动配置值以进行转换.标准库有多个易出错的消费函数,它们使用的模式是返回Result,返回Err个 case 中可能消费的值.例如,Arc::try_unwrap提取Arc的内值,但如果此操作失败,则返回Err变体中的Arc.

我们可以在这里执行相同的操作,创建一个函数,该函数在文件名匹配的情况下生成一个适当的 struct ,但在出现错误时返回配置:

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

然后,您可以使用该函数的特定实例化的静态切片编写另一个函数,并且它可以按顺序try 每个函数,直到一个函数成功.因为我们将配置移动到每个加载器函数中,所以我们必须将其放回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)

Rust相关问答推荐

什么时候铁 rust FFI边界上的panic 是未定义的行为?

程序在频道RX上挂起

为什么`str`类型可以是任意大小(未知大小),而`string`类型的大小应该是已知的?

为什么&;mut buf[0..buf.len()]会触发一个可变/不可变的borrow 错误?

为相同特征的特征对象使用 move 方法实现特征

可以在旋转循环中调用try_recv()吗?

为什么我可以使用 &mut (**ref) 创建两个实时 &mut 到同一个变量?

Rust 文件未编译到 dll 中

如何获取模块树?

Rust: 目标成员属于哪个"目标家族"的列表是否存在?

我如何取消转义,在 Rust 中多次转义的字符串?

在多核嵌入式 Rust 中,我可以使用静态 mut 进行单向数据共享吗?

使用 lalrpop 在 rust 中解析由 " 引用的字符串

一个函数调用会产生双重borrow 错误,而另一个则不会

在 Rust 中,我如何处理请求 javascript 的页面?

不能将 `*self` borrow 为不可变的,因为它也被borrow 为可变的 - 编译器真的需要如此严格吗?

为什么-x试图解析为文字并在声明性宏中失败?

在 Rust 中有条件地导入?

如何制作具有关联类型的特征的类型擦除版本?

有没有比多个 push_str() 调用更好的方法将字符串链接在一起?