如何避免将具体结构体更改为通用类型所带来的连锁反应?

9

我有一个配置结构体,长这样:

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的,而实际的构建器实现更复杂,并且在构建器中有几个函数和一些状态,这使得闭包方法难以处理。

1
不需要绑定 Sized;这是默认设置。 - Shepmaster
2个回答

10

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

话虽如此,它也可能很恼人!如果您只有少量实现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<MyBuilders>,你可以使用类型别名来隐藏它。

当我想要在测试期间注入测试实现代码,但是在生产代码中使用一组固定的实现时,我已经成功地使用了这个技巧。

enum_dispatch crate有助于构建此模式。


9
你可以使用 trait对象 Box<dyn ListBuilder> 来隐藏构建器的类型。这样做会带来一些影响,如动态调度(对build方法的调用将经过虚函数表)、额外的内存分配(封装的trait对象)以及一些有关于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,我使用了这里描述的方法来解决:https://users.rust-lang.org/t/solved-is-it-possible-to-clone-a-boxed-trait-object/1714/6 - mushin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接