#string #index #unique #unique-identifier #string-interning #duplicate #intern

dup-indexer

从字符串、静态 str、Vec 或 Box 值创建非重复索引

9 个不稳定版本 (3 个破坏性更新)

0.4.0 2023 年 11 月 27 日
0.3.2 2023 年 8 月 10 日
0.3.0 2023 年 4 月 12 日
0.2.3 2023 年 4 月 10 日
0.1.0 2023 年 4 月 4 日

#651 in 数据结构

Download history 91/week @ 2024-04-08 80/week @ 2024-04-15 85/week @ 2024-04-22 56/week @ 2024-04-29 67/week @ 2024-05-06 199/week @ 2024-05-13 213/week @ 2024-05-20 204/week @ 2024-05-27 207/week @ 2024-06-03 192/week @ 2024-06-10 224/week @ 2024-06-17 166/week @ 2024-06-24 147/week @ 2024-07-01 153/week @ 2024-07-08 197/week @ 2024-07-15 197/week @ 2024-07-22

每月下载量 699
5 个 crate 中使用 (通过 geozero)

MIT/Apache

28KB
419

Dup-Indexer

GitHub crates.io version docs.rs docs crates.io version CI build

创建一个没有额外内存分配的非重复值向量,即使对于像 StringVec 这样的 ref 值也是如此。每次插入都返回插入值的 usize 索引。完成时,可以使用整个向量。

这种方法对于创建唯一值的向量很有用,例如唯一字符串列表或唯一对象列表,然后使用向量中的值的索引作为唯一标识符,例如在 protobuf 消息中。

此 crate 中有两个对象

  • DupIndexer<T> - 使用 insert(value: T) 添加值,其中 value 在每次调用时所有权被移动到索引器中。这对于插入后不再需要值的情况很有用,或者对于实现 Copy 的值。
  • DupIndexerRefs<T: Deref> - 使用 insert_owned(value: T) 和/或 insert_ref(value: &T::Target),要么通过所有权转移插入(就像 DupIndexer 一样),要么通过引用插入,并且只有当值不在索引中时才复制该值。这仅适用于 String/&str 对,或者可以为其自定义类型实现。

示例

use dup_indexer::{DupIndexer, DupIndexerRefs};

fn main() {
  let mut di = DupIndexerRefs::new();
  assert_eq!(di.insert_owned("hello".to_string()), 0);
  assert_eq!(di.insert_ref("world"), 1);
  assert_eq!(di.insert_ref("hello"), 0);
  assert_eq!(di.into_vec(), vec!["hello", "world"]);
  
  // with strings
  let mut di = DupIndexer::new();
  assert_eq!(di.insert("hello".to_string()), 0);
  assert_eq!(di.insert("world".to_string()), 1);
  assert_eq!(di.insert("hello".to_string()), 0);
  assert_eq!(di.into_vec(), vec!["hello", "world"]);

  // with i32
  let mut di = DupIndexer::with_capacity(10);
  assert_eq!(di.insert(42), 0);
  assert_eq!(di.insert(13), 1);
  assert_eq!(di.insert(42), 0);
  assert_eq!(di[1], 13); // use it as a read-only vector
  assert_eq!(di.into_iter().collect::<Vec<_>>(), vec![42, 13]);

  //
  // with custom enum
  //
  #[derive(Debug, Eq, PartialEq, Hash, Clone)]
  enum Value {
    Str(String),
    Int(i32),
  }

  // All values inside the Value enum implement PtrRead
  unsafe impl dup_indexer::PtrRead for Value {}

  let mut di: DupIndexer<Value> = DupIndexer::new();
  assert_eq!(di.insert(Value::Str("foo".to_string())), 0);
  assert_eq!(di.insert(Value::Int(42)), 1);
  assert_eq!(di.insert(Value::Str("foo".to_string())), 0);
  assert_eq!(di[1], Value::Int(42));
  assert_eq!(
    di.into_vec(),
    vec![Value::Str("foo".to_string()), Value::Int(42)]
  );
}

实现

DupIndexer 按插入顺序在向量中保持插入的值。它还在查找映射 HashMap<T, usize> 中跟踪插入的值,其中 T 是插入值的类型。这意味着插入的值必须实现 HashEq

像整型、浮点型、布尔型、字符型这样的值类型以及任何像 &str 这样的引用都不会引起问题,因为它们可以复制到向量和查找映射容器中。然而,像 StringVec 这样的不可复制的具有内存分配的类型不能同时被两个容器拥有。为了解决这个问题,DupIndexer 创建了值的浅拷贝,并将其存储在哈希映射中,而原始值则放入向量中。

use std::collections::hash_map::{Entry, HashMap};
use std::mem::ManuallyDrop;
use std::hash::Hash;

pub unsafe trait PtrRead {}

pub struct DupIndexer<T> {
  values: Vec<T>,
  lookup: HashMap<ManuallyDrop<T>, usize>,
}

impl<T: Hash + Eq + PtrRead> DupIndexer<T> {
  pub fn insert(&mut self, value: T) -> usize {
    let dup_value = ManuallyDrop::new(unsafe { std::ptr::read(&value) });
    match self.lookup.entry(dup_value) {
      Entry::Occupied(entry) => *entry.get(),
      Entry::Vacant(entry) => {
        let index = self.values.len();
        entry.insert(index);
        self.values.push(value);
        index
      }
    }
  }
}

这样,哈希映射拥有浅拷贝,向量拥有原始值。在后续调用中,新值将检查哈希映射是否存在重复项。完成后,用户使用 .into_vec() 消耗具有键的向量,而哈希映射则在丢弃实际键之前被丢弃。

安全性

DupIndexerRefs 假定它是安全的,因为只要 String 本身没有修改,即使 String 对象存储在可能调整大小的 Vec<String> 中,对 String 的解引用(&str)就是有效的。

类似地,DupIndexer 是安全的,因为哈希映射只保留在拥有它时由 ptr:read 创建的原始值的副本,而且值永远不会被修改。一些类型,如 Box 可能会复杂一些,因此为了安全性/让 Miri 满意,这个库有一个不安全的 PtrRead 标记特质,大多数基本类型都实现了它。

Miri 通过了所有测试,但如果 TBox<i32>,则会失败。当测试尝试插入重复值时,我会看到以下 Miri 警告。如果您知道这是否真的是一个问题以及如何修复它,请告诉我。

❯ cargo +nightly miri test

    --> .../.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1328:23
     |
1328 |         PartialEq::eq(&**self, &**other)
     |                       ^^^^^^^
     |                       |
     |                       trying to retag from <170684> for SharedReadOnly permission at alloc64636[0x0], but that tag does not exist in the borrow stack for this location
     |                       this error occurs as part of retag at alloc64636[0x0..0x4]
     |
     = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
     = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <170684> was created by a Unique retag at offsets [0x0..0x4]
    --> src/lib.rs:49:17
     |
49   |                 entry.insert(index);
     |                 ^^^^^^^^^^^^^^^^^^^
help: <170684> was later invalidated at offsets [0x0..0x4] by a Unique retag
    --> src/lib.rs:50:34
     |
50   |                 self.values.push(value);
     |                                  ^^^^^

开发

  • 这个项目更容易与 just 开发,它是对 make 的现代替代品。使用 cargo install just 安装它。
  • 要获取可用命令的列表,请运行 just
  • 要运行测试,请使用 just test
  • 在执行 git push 时,它将执行一些验证,包括 cargo fmtcargo clippycargo test。使用 git push --no-verify 来跳过这些检查。
  • 要运行基准测试,使用 just bench
  • 要使用 Miri 进行测试,使用 just miri(注意,由于上述问题,其中一个测试被禁用)。

许可证

以下任一许可证下授权:

贡献

除非您明确表示,否则根据 Apache-2.0 许可证定义的,您有意提交的工作中的任何贡献都应如上双重许可,不附加任何额外条款或条件。

无运行时依赖