如何用两个键实现HashMap?

lsu*_*nsi 22 hashmap rust

HashMap实现getinsert方法,它们分别采用单个不可变借位和单个值的移动.

我想要一个像这样的特性,但需要两个键而不是一个.它使用里面的地图,但它只是实现的细节.

pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    map: HashMap<(A, B), f64>,
}

impl<A: Eq + Hash, B: Eq + Hash> Memory<A, B> for Table<A, B> {
    fn get(&self, a: &A, b: &B) -> f64 {
        let key: &(A, B) = ??;
        *self.map.get(key).unwrap()
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        self.map.insert((a, b), v);
    }
}
Run Code Online (Sandbox Code Playgroud)

ken*_*ytm 24

这当然是可能的.该签名get就是

fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V> 
where
    K: Borrow<Q>,
    Q: Hash + Eq, 
Run Code Online (Sandbox Code Playgroud)

这里的问题是实现这样的&Q类型

  1. (A, B): Borrow<Q>
  2. Q 器物 Hash + Eq

为了满足条件(1),我们需要考虑如何写

fn borrow(self: &(A, B)) -> &Q
Run Code Online (Sandbox Code Playgroud)

诀窍是&Q 不需要是一个简单的指针,它可以是一个特质对象!我们的想法是创建一个Q具有两个实现的特征:

impl Q for (A, B)
impl Q for (&A, &B)
Run Code Online (Sandbox Code Playgroud)

Borrow实施将简单的返回self,我们可以构建一个&Q由独立的两个元素特质的对象.


全面实施是这样的:

use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::borrow::Borrow;

// See explanation (1).
#[derive(PartialEq, Eq, Hash)]
struct Pair<A, B>(A, B);

#[derive(PartialEq, Eq, Hash)]
struct BorrowedPair<'a, 'b, A: 'a, B: 'b>(&'a A, &'b B);

// See explanation (2).
trait KeyPair<A, B> {
    /// Obtains the first element of the pair.
    fn a(&self) -> &A;
    /// Obtains the second element of the pair.
    fn b(&self) -> &B;
}

// See explanation (3).
impl<'a, A, B> Borrow<KeyPair<A, B> + 'a> for Pair<A, B>
where
    A: Eq + Hash + 'a,
    B: Eq + Hash + 'a,
{
    fn borrow(&self) -> &(KeyPair<A, B> + 'a) {
        self
    }
}

// See explanation (4).
impl<'a, A: Hash, B: Hash> Hash for (KeyPair<A, B> + 'a) {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.a().hash(state);
        self.b().hash(state);
    }
}

impl<'a, A: Eq, B: Eq> PartialEq for (KeyPair<A, B> + 'a) {
    fn eq(&self, other: &Self) -> bool {
        self.a() == other.a() && self.b() == other.b()
    }
}

impl<'a, A: Eq, B: Eq> Eq for (KeyPair<A, B> + 'a) {}

// OP's Table struct
pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    map: HashMap<Pair<A, B>, f64>,
}

impl<A: Eq + Hash, B: Eq + Hash> Table<A, B> {
    fn new() -> Self {
        Table { map: HashMap::new() }
    }

    fn get(&self, a: &A, b: &B) -> f64 {
        *self.map.get(&BorrowedPair(a, b) as &KeyPair<A, B>).unwrap()
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        self.map.insert(Pair(a, b), v);
    }
}

// Boring stuff below.

impl<A, B> KeyPair<A, B> for Pair<A, B>
where
    A: Eq + Hash,
    B: Eq + Hash,
{
    fn a(&self) -> &A {
        &self.0
    }
    fn b(&self) -> &B {
        &self.1
    }
}
impl<'a, 'b, A, B> KeyPair<A, B> for BorrowedPair<'a, 'b, A, B>
where
    A: Eq + Hash + 'a,
    B: Eq + Hash + 'b,
{
    fn a(&self) -> &A {
        self.0
    }
    fn b(&self) -> &B {
        self.1
    }
}

//----------------------------------------------------------------

#[derive(Eq, PartialEq, Hash)]
struct A(&'static str);

#[derive(Eq, PartialEq, Hash)]
struct B(&'static str);

fn main() {
    let mut table = Table::new();
    table.set(A("abc"), B("def"), 4.0);
    table.set(A("123"), B("456"), 45.0);
    println!("{:?} == 45.0?", table.get(&A("123"), &B("456")));
    println!("{:?} == 4.0?", table.get(&A("abc"), &B("def")));
    // Should panic below.
    println!("{:?} == NaN?", table.get(&A("123"), &B("def")));
}
Run Code Online (Sandbox Code Playgroud)

说明:

  1. 我们介绍了PairBorrowedPair类型.(A, B)由于孤儿规则E0210,我们无法直接使用.这很好,因为地图是一个实现细节.

  2. 这个KeyPair特性扮演了Q我们上面提到的角色.我们需要impl Eq + Hash for KeyPair,但Eq并且Hash都不是对象安全的.我们添加a()b()方法来帮助手动实现它们.

  3. 现在我们实行Borrow的特质Pair<A, B>KeyPair + 'a.注意'a- 这是Table::get实际工作所需的微妙位.任意性'a允许我们说a Pair<A, B>可以在任何生命周期中借用到特征对象.如果我们没有指定'a,那么unsized trait对象将默认为'static,这意味着Borrow只有当实现像BorrowedPairoutlives 时才能应用trait 'static,这当然不是这种情况.

  4. 最后,我们实施EqHash.如上所述,我们实现KeyPair + 'a而不是KeyPair(KeyPair + 'static在这种情况下意味着).


在计算哈希值和检查相等性时,使用特征对象会产生间接成本get().如果优化器能够将其虚拟化,则可以消除成本,但LLVM是否会执行此操作尚不清楚.

另一种方法是将地图存储为HashMap<(Cow<A>, Cow<B>), f64>.使用此需要较少的"聪明代码",但现在有存储资/借来的标志,以及在运行时的成本存储成本get()set().

除非你分叉标准HashMap并添加一个Hash + Eq单独查找条目的方法,否则没有保证零成本的解决方案.

  • 是的,疯狂的道具。这是对这个问题的一个很好的回答,同时也是初学者如何利用和理解 Rust 特征系统的一个很好的例子 (3认同)
  • 我对这个答案的完整和详细程度感到惊讶.读了三遍之后(那是Rust,不是你),我想我明白了.是的,我以为我错过了一些东西,但事实证明这个问题有点棘手.非常感谢. (2认同)

dto*_*nay 6

在该get方法中,在存储器中借用的值ab可能彼此不相邻的值.

[--- A ---]      other random stuff in between      [--- B ---]
 \                                                 /
  &a points to here                               &b points to here
Run Code Online (Sandbox Code Playgroud)

借用类型的值&(A, B)将需要a A和a B相邻.

     [--- A ---][--- B ---]
      \
       we could have a borrow of type &(A, B) pointing to here
Run Code Online (Sandbox Code Playgroud)

一些不安全的代码可以解决这个问题!我们需要一个浅的副本*a*b.

use std::collections::HashMap;
use std::hash::Hash;
use std::mem::ManuallyDrop;
use std::ptr;

#[derive(Debug)]
pub struct Table<A: Eq + Hash, B: Eq + Hash> {
    map: HashMap<(A, B), f64>
}

impl<A: Eq + Hash, B: Eq + Hash> Table<A, B> {
    fn get(&self, a: &A, b: &B) -> f64 {
        unsafe {
            // The values `a` and `b` may not be adjacent in memory. Perform a
            // shallow copy to make them adjacent. This should be fast! This is
            // not a deep copy, so for example if the type `A` is `String` then
            // only the pointer/length/capacity are copied, not any of the data.
            //
            // This makes a `(A, B)` backed by the same data as `a` and `b`.
            let k = (ptr::read(a), ptr::read(b));

            // Make sure not to drop our `(A, B)`, even if `get` panics. The
            // caller or whoever owns `a` and `b` will drop them.
            let k = ManuallyDrop::new(k);

            // Deref `k` to get `&(A, B)` and perform lookup.
            let v = self.map.get(&k);

            // Turn `Option<&f64>` into `f64`.
            *v.unwrap()
        }
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        self.map.insert((a, b), v);
    }
}

fn main() {
    let mut table = Table { map: HashMap::new() };
    table.set(true, true, 1.0);
    table.set(true, false, 2.0);

    println!("{:#?}", table);

    let v = table.get(&true, &true);
    assert_eq!(v, 1.0);
}
Run Code Online (Sandbox Code Playgroud)