如何避免将具体结构更改为通用结构所产生的连锁反应?

How can I avoid a ripple effect from changing a concrete struct to generic?

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

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 class 的大多数用法都不关心 ListBuilder - 它是一个实现细节。我担心如果我向 Conf 添加另一个泛型类型,现在我必须返回并在所有地方添加另一个泛型参数。有什么办法可以避免这种情况吗?

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

您可以使用 trait object Box<dyn ListBuilder> to hide the type of the builder. Some of the consequences are dynamic dispatch (calls to the build method will go through a virtual function table), additional memory allocation (boxed trait object), and some 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
    }
}

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

话虽如此,这可能很烦人!如果你有少量类型实现你的特征,你也可以构建这些类型的枚举并委托给子实现:

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<MyBuilders>,您可以使用类型别名来隐藏它。

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

enum_dispatch crate 有助于构建此模式。