在单个 Hashmap 中存储不同类型

Storing different types in a single Hashmap

一些防止xy的上下文:

我想为我的系统建立一个缓存。在 1:1 关系中存在由 IdTypeEndpointType 组成的类型对。每个 IdType 仅指代一个 EndpointType,反之亦然。 Id 特性(见下面的代码)不是必需的,它只存在于我当前尝试使它工作的迭代中。

有太多不同类型的端点——通常一个应用程序只会使用它们的一小部分——以证明静态地为每个端点构建一个缓存并将其保存在内存中是合理的。此外,我希望能够在不触及与缓存或客户端本身相关的任何代码的情况下添加新端点。

Endpoint 特征对象不是对象安全的,并且由于关联的 consts、Sized 和不使用 [=19= 的方法,无法使它们成为对象安全的] 以利用编译时优化。

我想到了将它们存储为 Any 类型的想法。现在我喜欢类型安全,所以我试着限制它多一点。在没有找到满意的解决方案后,我的想法还有几个问题:

  1. 如何使这种方法起作用?
  2. 有更好的解决方案吗?
  3. 这是声音吗?为什么?
  4. 有什么办法让它听起来好吗?
  5. 我可以在没有不安全的情况下实现这个吗?

Link to the rust playground

use std::sync::Mutex;
use std::collections::HashMap;

pub trait Endpoint: Sized {
    type Id;
}

pub trait Id {
    type Endpoint;
}

pub struct Client {
    cache: Mutex<Cache>,
}

impl Client {
    fn get<T: Endpoint>(&self, id: T::Id) -> T {
        if let Some(result) = self.cache.lock().unwrap().get(id) {
            return result;
        }
        todo!()
    }
}

pub struct Cache {
    map: HashMap<Box<dyn Id>, Box<dyn Endpoint>>,
}

impl Cache {
    fn get<T: Id>(&self, id: T) -> Option<T::Endpoint> {
        if let Some(endpoint) = self.map.get(Box::new(id)) {
            let endpoint: Box<T::Endpoint> = unsafe { std::mem::transmute(endpoint) };
            Some(endpoint)
        } else {
            None
        }
    }
}

我强烈建议使用 Box<dyn Any> 而不是 std::mem::transmute。一个相当常见的模式是 HashMap<TypeId, Box<dyn Any>>TypeId 是一种可以用来区分其他类型的类型,它的 EqHash impls 的工作方式与您期望的一样:如果类型相同,则类型 id 为平等的。所以你可以大致这样:

struct Cache {
  map: HashMap<TypeId, Box<dyn Any>>,
}

impl Cache {
  fn get<T>(&self) -> Option<&T> {
    let type_id = TypeId::of::<T>();
    let any = self.map.get(&type_id)?;
    any.downcast_ref()
  }
}

这类似于粗略查找 table,将类型与该类型的单个值相关联。如果 Box<dyn Any>T 的类型 ID 相关联,但指向其他类型的值,则您不会获得 UB,只会获得 None .

这种原语可用于围绕它构建更复杂的缓存。例如,您可以有一个结构来包装提供访问权限的 this,但它只支持 (T, <T as Id>::Endpoint).

类型的键

重点是使用Anydowncast_*方法来避免不安全

std::mem::transmute 是更危险的不安全函数之一,在很大程度上应被视为最后的手段。来自文档:

transmute is incredibly unsafe. There are a vast number of ways to cause undefined behaviour with this function. transmute should be the absolute last resort

我还要补充一点,这种模式很常见,可能有一个提供类型安全接口的箱子,快速搜索得到了这个:https://crates.io/crates/anymap,虽然我不能说这个箱子的质量特别是

编辑:

如果您还希望能够区分相同 Id/Endpoint 对类型的多个实例,您可以修改它以存储具有类型 (TypeId, u64),其中 u64 是对原始密钥进行哈希处理的结果(有点像 SQL 复合密钥):

struct Cache {
  map: HashMap<(TypeId, u64), Box<dyn Any>>,
}

impl Cache {
    fn insert<T>(&mut self, id: T::Id, endpoint: T)
    where
        T: Endpoint + 'static,
        T::Id: Hash,
    {
        let type_id = TypeId::of::<T>();
        let hash = {
            let mut hasher = DefaultHasher::new();
            id.hash(&mut hasher);
            hasher.finish()
        };

        self.map.insert((type_id, hash), Box::new(endpoint));
    }

    fn get<T>(&self, id: T::Id) -> Option<&T>
    where
        T: Endpoint + 'static,
        T::Id: Hash,
    {
        let type_id = ...;
        let hash = ...;

        let option_any = self.map.get(&(type_id, hash));
        option_any.and_then(|any| any.downcast_ref())
    }
}

这让你有多个 FooEndpoints(有 FooIds),以及 BarEndpoints(和 BarIds),同时避免 transmute/unsafe,全部通过一次地图查找。希望这次我能更准确地阅读你的问题;p

P.S。我不知道这种获取 u64 散列的特殊方式是否“好”,我以前从来没有真正这样做过(我只使用过 Hash 特性和 std::collections::HashMap/HashSet).可能值得做一些谷歌搜索以确保这不会做一些可怕的事情