1 个稳定版本

1.0.7 2024 年 3 月 26 日

#1247数据结构


tgf 中使用

Zlib 许可证

280KB
4.5K SLoC

slotmap - 但可映射

slotmap 包的分支,但增加了一个 map 函数,可以将一种类型的槽映射映射到另一种类型,同时保留相同的键索引。


lib.rs:

slotmap

此库提供了一个具有持久唯一键的容器,用于访问存储的值,SlotMap。插入时返回一个键,可用于以后访问或删除值。插入、删除和访问都只需 O(1) 时间,开销低。非常适合存储需要稳定、安全引用但其他方面没有明确所有权的对象集合,例如游戏实体或图节点。

BTreeMapHashMap 与槽映射的区别在于,槽映射在插入值时生成并返回键。键始终是唯一的,并且只会引用已插入的值。槽映射的主要目的是以安全和高效的方式拥有事物。

您还可以创建(多个)二级映射,可以将 SlotMap 返回的键映射到其他值,以将任意数据与槽映射中存储的对象关联起来,无需哈希 - 它是底层的直接索引。

此包所需的最小稳定 Rust 版本为 1.49。

示例

let mut sm = SlotMap::new();
let foo = sm.insert("foo");  // Key generated on insert.
let bar = sm.insert("bar");
assert_eq!(sm[foo], "foo");
assert_eq!(sm[bar], "bar");

sm.remove(bar);
let reuse = sm.insert("reuse");  // Space from bar reused.
assert_eq!(sm.contains_key(bar), false);  // After deletion a key stays invalid.

let mut sec = SecondaryMap::new();
sec.insert(foo, "noun");  // We provide the key for secondary maps.
sec.insert(reuse, "verb");

for (key, val) in sm {
    println!("{} is a {}", val, sec[key]);
}

通过 serdeno_std 支持和不稳定功能进行序列化

键和槽映射都支持通过 serde 库进行(反)序列化。即使一个或两个已经序列化和反序列化,键对槽映射仍然有效!这使得存储或传输复杂的引用结构和图变得非常简单。已经注意到了从不可信来源反序列化键和槽映射的安全性。如果您想使用这些功能,您必须在您的 Cargo.toml 中为 slotmap 启用 serde 功能标志。

slotmap = { version = "1.0", features = ["serde"] }

此软件包还支持 no_std 环境,但需要 alloc 软件包可用。要启用此功能,您必须禁用默认启用的 std 功能。

slotmap = { version = "1.0", default-features = false }

遗憾的是,SparseSecondaryMapno_std 中不可用,因为它依赖于 HashMap。最后,可以定义 unstable 功能以启用仅在 nightly Rust 中工作的 slotmap 的部分。

为什么不用索引 Vec,或者使用 slabstable-vec 等?

这些解决方案要么不能从已删除的元素中回收内存,要么受到 ABA 问题的困扰。由 slotmap 返回的键是版本化的。这意味着一旦键被删除,它就会保持删除状态,即使槽映射内部的物理存储被重新用于新元素。键是插入值的永久唯一引用*。尽管支持版本化,但 SlotMap 通常并不比替代方案(多)慢,因为它内部使用经过仔细检查的不安全代码。最后,slotmap 简单地拥有许多使您的生活变得容易的功能。

性能特征和实现细节

插入、访问和删除都是 O(1),并且通过将元素存储在 Vec 中具有低开销。与引用或向量的索引不同,除非您删除一个键,否则它永远不会失效。在幕后,向量中的每个槽都是一个 (value, version) 元组。插入后,返回的键也包含一个版本。只有当存储的版本和键中的版本匹配时,键才有效。这允许我们在删除后重新使用向量中的空间,而不会让已删除的键指向虚假的新元素。 *经过 231 次对相同基本槽的删除和插入后,版本会回绕,这样虚假的引用可能会发生。然而,这种情况发生的可能性极低,并且在所有情况下行为都是安全的。槽映射可以同时容纳多达 232 - 2 个元素。

SlotMap中,每个槽位的内存使用量为4 + max(sizeof(T), 4),向上取整到类型的对齐方式。类似地,对于HopSlotMap,内存使用量为4 + max(sizeof(T), 12)。《DenseSlotMap》每个元素和每个槽位额外占用8字节。

选择SlotMapHopSlotMapDenseSlotMap

SlotMap在大多数操作中速度最快,除了迭代。它不能缩小其底层存储的大小,因为它必须记住每个存储槽位中最后一次存储的版本,即使该槽位现在为空。这意味着迭代可能会很慢,因为它必须遍历可能很多空槽位。

HopSlotMap通过在插入/删除时维护更多信息来解决此问题,允许它通过“跳过”连续的空槽位块来迭代仅包含填充槽位。这可以使迭代速度显著提高。如果您预计将大量迭代SlotMap中的所有元素,并且可能删除大量元素,请选择HopSlotMap。缺点是插入和删除速度大约慢两倍。随机访问速度相同。

DenseSlotMap更进一步,将所有元素存储在连续的内存块中。它对每个随机访问使用两个间接引用;槽位包含用于访问连续内存的索引。这意味着随机访问速度比SlotMapHopSlotMap都要慢,但迭代速度显著提高,与普通Vec一样快。

选择SecondaryMapSparseSecondaryMap

您想将额外数据与存储在槽位图中的对象关联起来,因此您使用(多个)次级映射将键映射到该数据。

一个 SecondaryMap 实际上是一个和槽映射(slot map)类似的槽数组,本质上提供了和 SlotMap 相同的操作保证(除了你需要提供由主槽映射生成的键)。这意味着即使你只关联主槽映射中的一个元素的数据,你可能需要并且必须初始化和原始一样多的内存。

一个 SparseSecondaryMap 类似于一个从键到对象的 HashMap,然而它会自动删除已重新使用空间的槽的过时键。如果你预期只存储主槽映射的一小部分关联数据,你应该使用这个变体。

自定义键类型

如果你有多个槽映射,使用一个槽映射的键在另一个槽映射上是一个错误。结果是安全的,但未指定,并且无法在运行时检测,因此可能导致难以发现的错误。

为了防止这种情况,槽映射允许你指定它们返回的键的类型。你可以使用 new_key_type! 宏来构造新的键类型。生成的类型的行为完全像 DefaultKey,但是一个不同的类型。所以,你不会简单地使用 SlotMap<DefaultKey, Player>,而是会使用

new_key_type! { struct PlayerKey; }
let sm: SlotMap<PlayerKey, Player> = SlotMap::with_key();

你可以使用 [Key] 特性来编写针对任何键类型的泛型代码。

依赖

~175KB