Store a collection of heterogeneous types with gen

2020-07-24 04:47发布

问题:

I'm trying to implement a basic ECS in Rust. I want a data structure storing, for each component, a storage of that particular component. Because some components are common while others are rare, I want different types of storage policies such as VecStorage<T> and HashMapStorage<T>.

As components are unknown to the game engine's ECS, I came up with:

trait AnyStorage: Debug {
    fn new() -> Self
    where
        Self: Sized;
}

#[derive(Default, Debug)]
struct StorageMgr {
    storages: HashMap<TypeId, Box<AnyStorage>>,
}

with VecStorage and HashMapStorage<T> implementing the AnyStorage trait. Since AnyStorage doesn't know T, I added one more trait implemented by both concrete storages: ComponentStorage<T>.

While I was able to register new components (i.e. add a new Box<AnyStorage> in StorageMgr's storages), I didn't find a way to insert components.

Here is the erroneous code:

pub fn add_component_to_storage<C: Component>(&mut self, component: C) {
    let storage = self.storages.get_mut(&TypeId::of::<C>()).unwrap();
    // storage is of type: &mut Box<AnyStorage + 'static>

    println!("{:?}", storage); // Prints "VecStorage([])"

    storage.insert(component); // This doesn't work

    // This neither:
    // let any_stor: &mut Any = storage;
    // let storage = any_stor.downcast_ref::<ComponentStorage<C>>();
}

I know that my problem comes from the fact that storage's type is &mut Box<AnyStorage>; can I obtain the concrete VecStorage from it?

The whole point of doing all this is that I want components to be contiguous in memory and to have different storage for each component type. I can not resolve myself to use Box<Component>, or I don't see how.

I reduced my problem to a minimal code on Rust Playground.

回答1:

I wasn't sure if something like this was possible but I've finally figured it out. There are a couple things to note as to why your posted example was failing.

  1. Trait AnyStorage in your example did not implement ComponentStorage<T>, therefore because you were storing your "storage"s in a HashMap<TypeId, Box<AnyStorage>>, Rust could not guarantee that every stored type implemented ComponentStorage<T>::insert() because it only knew that they were AnyStorages.
  2. If you did combine the two traits into one simply called Storage<T> and stored them in a HashMap<TypeId, Box<Storage<T>>, every version of Storage would have to store the same type because of the single T. Rust doesn't have a way to dynamically type the values of a map based on the TypeId of the key, as a solution like this would require. Also, you can't replace T with Any because Any isn't Sized, which Vec and all other storage types require. I'm guessing you knew all of this which is why you used two different traits in your original example.

The solution I ended up using stored the Storage<T>s as Anys in a HashMap<TypeId, Box<Any>>, and then I downcasted the Anys into Storage<T>s inside the implementation functions for StorageMgr. I've put a short example below, and a full version is on Rust Playground here .

trait Component: Debug + Sized + Any {
    type Storage: Storage<Self>;
}

trait Storage<T: Debug>: Debug + Any {
    fn new() -> Self
    where
        Self: Sized;

    fn insert(&mut self, value: T);
}

struct StorageMgr {
    storages: HashMap<TypeId, Box<Any>>,
}

impl StorageMgr {
    pub fn new() -> Self {
        Self {
            storages: HashMap::new(),
        }
    }

    pub fn get_storage_mut<C: Component>(&mut self) -> &mut <C as Component>::Storage {
        let type_id = TypeId::of::<C>();

        // Add a storage if it doesn't exist yet
        if !self.storages.contains_key(&type_id) {
            let new_storage = <C as Component>::Storage::new();

            self.storages.insert(type_id, Box::new(new_storage));
        }

        // Get the storage for this type
        match self.storages.get_mut(&type_id) {
            Some(probably_storage) => {
                // Turn the Any into the storage for that type
                match probably_storage.downcast_mut::<<C as Component>::Storage>() {
                    Some(storage) => storage,
                    None => unreachable!(), // <- you may want to do something less explosive here
                }
            }
            None => unreachable!(),
        }
    }
}


标签: rust