在 Rust 中使用泛型、特征别名和构造函数

Using generics, trait aliases, and constructors in Rust

上下文

我有一个 DataStore<Key,Value> 特性可以抽象出数据存储。 (例如,我可以为包装 VecsHashMaps 的数据存储创建此特征的简单实现。)我想要这种抽象,因为一些使用 cases/targets 需要小但计算效率低下的存储和其他允许更大的商店。 (编辑:在下面的特征定义中添加了对 self 的引用。)

// Stores data of type V indexed by K
trait DataStore<K, V> {
    fn new() -> Self;
    fn get(&self, k: K) -> Option<&V>;
    fn insert(&mut self, v: V) -> Option<K>;
}

我现在想定义一个包含两个数据存储的结构 Thing:一个存储在一种类型 Apple<T> 上,另一个存储在另一种类型 Banana<T> 上。这是我的第一次尝试,

// some objects I'd like to keep in DataStores
struct Apple<T> { shine: T }
struct Banana<T> { spottedness: T }

// Attempt #1: cumbersome, have to always specify generic constraints when 
//             using Thing elsewhere
pub struct Thing<K, T, AppleStore, BananaStore>
    where AppleStore: DataStore<K, Apple<T>>,
          BananaStore: DataStore<K, Banana<T>>
{
    apple_store: AppleStore,
    banana_store: BananaStore,
}

这种方法使用起来很麻烦,因为每当我想将 Thing 传递给函数或实现 Thing 的特征时,我必须始终输入 <K, T, AppleStore, BananaStore> where ... 即使说function 或 trait 不关心这两个商店中的任何一个。例如,如果我想为 Thing 实现一个特征,它对类型为 T 的其他属性执行一些不相关的操作,我仍然必须告诉它关于 KAppleStore、和 BananaStore.

我了解了类型别名并尝试了以下方法:


// Attempt #2: looks easier to use. only two generics on Thing: the type of
//             the indexes and the type of the internal parameters. not sure
//             about the role of dyn, though, since this should be checkable
//             at compile time
type AppleStore<K, T> = dyn DataStore<K, Apple<T>>;
type BananaStore<K, T> = dyn DataStore<K, Banana<T>>;


pub struct Thing<K, T> {
    apple_store: AppleStore<K, T>,
    banana_store: BananaStore<K, T>,
}

当我尝试在 Thing 的构造函数中创建一个新的 BananaStore 时出现了一个新问题。这在尝试 #1 中是允许的,因为允许特征实现 (1) 不将 &self 作为参数和 (2) return 类型 Self 的函数。但这在尝试 #2 中是不允许的,因为动态特征需要 Sized 而 Self returns 不允许。 (或者什么?)


impl<K, T> Thing<K, T> {
    pub fn new(apple_store: AppleStore<K, T>) {
        Thing {
            apple_store: apple_store,
            banana_store: BananaStore::new() // not allowed to do this with
                                             // dynamic type aliases?
    }
}

问题

我是否需要在 Thing 之外创建一个 BananaStore 并将其作为参数传入,或者是否有办法从外部隐藏 BananaStore 的构造?如果我的目标之一是隐藏不必要的(可选的)对象创建,我想像 ThingBuilder 这样的东西可能是一种有效的方法。但我也不想提供 BananaStore 的默认实现者:用户应该明确声明哪种 DataStore 用于 BananaStore.

我以这种方式表述问题是因为最终我希望 ThingAppleStore 在多个 Thing 实例之间实际共享;也就是说,多个 Things 可以在商店中引用相同的 Apple<T>。但是每个 Thing 都会有自己的 BananaStore。我知道这将需要在 AppleStore 上使用 RcArc 或类似的东西,但我会在到达它时穿过那座桥。

DataStore

您的 DataStore 有一些问题给您带来了麻烦。 getset 函数需要引用 self 才能工作。没有对 self 的引用意味着他们能够从无到有地产生引用,并且会导致你以后的终身问题。 get 函数也应该接受一个引用来匹配 Map 的函数。通过添加引用,移除 K 实现 Copy 的约束,并防止未来出现生命周期问题。

trait DataStore<K, V> {
    // This will make it impossible to use this an anonymous trait like in attempt 2
    fn new() -> Self;

    // They need a reference to self to store and retrieve data
    fn get(&self, k: &K) -> Option<&V>;
    fn insert(&mut self, v: V) -> Option<K>;
}

从现在开始,这些小的变化应该有助于使调试更容易一些。

尝试 1

通过对 DataStore 的调整,我们已经解决了生命周期问题。您需要做的就是添加一个 PhantomData 来补偿额外的类型参数。

我还将删除示例中的 where 子句,因为我过去在将它添加到结构时遇到过问题。

// AppleStore and DataStore converted to single letters to make it a bit more concise
struct Thing<K, T, A: DataStore<K, Apple<T>>, B: DataStore<K, Banana<T>>> {
    apple_store: A,
    banana_store: B,
    _phantom: PhantomData<(K, T)>,
}

除此之外,这可能是您的不二之选。由于它不涉及像尝试 2 中那样的匿名特征,因此以后的生命周期应该不会有任何问题,并且更容易使用。

尝试 2

此尝试失败的原因是动态大小的匿名特征使编译器无法确定一个字段在内存中结束而下一个字段开始。我们可以通过首先装箱匿名特征来解决这个问题。您可以将其视为在结构中存储指针而不是实际数据。

pub struct Thing<K, T> {
    apple_store: Box<dyn DataStore<K, Apple<T>> + 'static>,
    banana_store: Box<dyn DataStore<K, Banana<T>> + 'static>,
}

'static 本质上只是意味着 DataStore 不会包含任何可能限制其生命周期的引用。您也可以让生命周期不受约束,但这几乎会挑衅地在未来给您带来更多问题。

pub struct Thing<'a, 'b, K, T> {
    apple_store: Box<dyn DataStore<K, Apple<T>> + 'a>,
    banana_store: Box<dyn DataStore<K, Banana<T>> + 'b>,
}