【问题标题】:How can I avoid a ripple effect from changing a concrete struct to generic?如何避免将具体结构更改为通用结构的连锁反应?
【发布时间】:2021-10-30 11:27:18
【问题描述】:

我有一个如下所示的配置结构:

struct Conf {
    list: Vec<String>,
}

实现是在内部填充list 成员,但现在我决定将该任务委托给另一个对象。所以我有:

trait ListBuilder {
    fn build(&self, list: &mut Vec<String>);
}

struct Conf<T: Sized + ListBuilder> {
    list: Vec<String>,
    builder: T,
}

impl<T> Conf<T>
where
    T: Sized + ListBuilder,
{
    fn init(&mut self) {
        self.builder.build(&mut self.list);
    }
}

impl<T> Conf<T>
where
    T: Sized + ListBuilder,
{
    pub fn new(lb: T) -> Self {
        let mut c = Conf {
            list: vec![],
            builder: lb,
        };
        c.init();
        c
    }
}

这似乎工作正常,但现在我使用Conf任何地方,我都必须更改它:

fn do_something(c: &Conf) {
    // ...
}

变成

fn do_something<T>(c: &Conf<T>)
where
    T: ListBuilder,
{
    // ...
}

因为我有很多这样的函数,所以这种转换很痛苦,尤其是Conf 类的大多数用法并不关心ListBuilder - 这是一个实现细节。我担心如果我向Conf 添加另一个泛型类型,现在我必须返回并在任何地方添加另一个泛型参数。有什么办法可以避免吗?

我知道我可以为列表构建器使用闭包,但我有一个附加约束,即我的 Conf 结构必须是 Clone,并且实际的构建器实现更复杂,并且有几个函数和一些构建器中的状态,这使得闭包方法变得笨拙。

【问题讨论】:

  • 不需要绑定Sized;这是默认设置。

标签: generics rust traits


【解决方案1】:

虽然泛型类型似乎会“感染”您的其余代码,但这正是它们有益的原因!编译器对所使用的类型的大小和具体类型的了解使其能够做出更好的优化决策。

话虽如此,这可能很烦人!如果您有少量实现您的 trait 的类型,您还可以构造这些类型的枚举并委托给子实现:

enum MyBuilders {
    User(FromUser),
    File(FromFile),
}

impl ListBuilder for MyBuilders {
    fn build(&self, list: &mut Vec<String>) {
        use MyBuilders::*;
        match self {
            User(u) => u.build(list),
            File(f) => f.build(list),
        }
    }
}

// Support code

trait ListBuilder {
    fn build(&self, list: &mut Vec<String>);
}

struct FromUser;
impl ListBuilder for FromUser {
    fn build(&self, list: &mut Vec<String>) {}
}

struct FromFile;
impl ListBuilder for FromFile {
    fn build(&self, list: &mut Vec<String>) {}
}

现在具体类型将是Conf&lt;MyBuilders&gt;,您可以使用类型别名来隐藏它。

当我希望能够在测试期间将测试实现注入代码中时,我使用它取得了很好的效果,但在生产代码中使用了一组固定的实现。

enum_dispatch crate 有助于构建这种模式。

【讨论】:

    【解决方案2】:

    您可以使用trait object Box&lt;dyn ListBuilder&gt; 隐藏构建器的类型。一些后果是动态调度(对build 方法的调用将通过一个虚函数表)、额外的内存分配(装箱的特征对象)和一些restrictions on the trait ListBuilder

    trait ListBuilder {
        fn build(&self, list: &mut Vec<String>);
    }
    
    struct Conf {
        list: Vec<String>,
        builder: Box<dyn ListBuilder>,
    }
    
    impl Conf {
        fn init(&mut self) {
            self.builder.build(&mut self.list);
        }
    }
    
    impl Conf {
        pub fn new<T: ListBuilder + 'static>(lb: T) -> Self {
            let mut c = Conf {
                list: vec![],
                builder: Box::new(lb),
            };
            c.init();
            c
        }
    }
    

    【讨论】:

    • 谢谢,这正是我所需要的。我的 ListBuilder 还需要实现 Clone,我使用这里描述的方法解决了这个问题:users.rust-lang.org/t/…
    猜你喜欢
    • 2015-04-05
    • 2021-11-08
    • 1970-01-01
    • 1970-01-01
    • 2023-01-27
    • 2018-08-16
    • 2023-03-23
    • 2014-07-27
    相关资源
    最近更新 更多