如何用两个键实现HashMap?

时间:2017-08-20 20:55:47

标签: 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);
    }
}

3 个答案:

答案 0 :(得分:23)

这当然是可能的。 signature of get

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

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

  1. (A, B): Borrow<Q>
  2. Q实施Hash + Eq
  3. 为了满足条件(1),我们需要考虑如何编写

    fn borrow(self: &(A, B)) -> &Q
    

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

    impl Q for (A, B)
    impl Q for (&A, &B)
    

    Borrow实现只会返回self,我们可以分别从这两个元素构建一个&Q特征对象。

    full implementation是这样的:

    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")));
    }
    

    说明:

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

    2. KeyPair特征扮演我们上面提到的Q角色。我们需要impl Eq + Hash for KeyPair,但EqHash都不是object safe。我们添加了a()b()方法来帮助您手动实施这些方法。

    3. 现在我们实施BorrowPair<A, B>KeyPair + 'a的特征。请注意'a - 这是使Table::get实际工作所需的微妙位。任意'a允许我们说可以将Pair<A, B>借用到任何生命周期的特征对象。如果我们未指定'a,则未确定的特征对象将default to 'static,这意味着仅当Borrow之类的实现超过BorrowedPair时才会应用'static特征当然不是这样的。

    4. 最后,我们实施了EqHash。如上所述,我们实施KeyPair + 'a而不是KeyPair(在此上下文中表示KeyPair + 'static)。

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

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

      除非您使用标准HashMap进行分叉并添加方法仅通过Hash + Eq查找条目,否则无法保证零成本解决方案。

答案 1 :(得分:5)

get方法中,ab借用的值在内存中可能不会彼此相邻。

[--- A ---]      other random stuff in between      [--- B ---]
 \                                                 /
  &a points to here                               &b points to here

借用&(A, B)类型的值需要AB相邻。

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

一些不安全的代码可以解决这个问题!我们需要*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);
}

答案 2 :(得分:0)

一个Memory特征,它带有两个键,按值设置引用

trait Memory<A: Eq + Hash, B: Eq + Hash> {

    fn get(&self, a: &A, b: &B) -> Option<&f64>;

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

您可以使用地图地图impl此类特征:

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

impl<A: Eq + Hash, B: Eq + Hash> Memory<A, B> for Table<A, B> {

    fn get(&self, a: &A, b: &B) -> Option<&f64> {
        self.table.get(a)?.get(b)
    }

    fn set(&mut self, a: A, b: B, v: f64) {
        let inner = self.table.entry(a).or_insert(HashMap::new());
        inner.insert(b, v);
    }
}

请注意,如果解决方案有点优雅,则必须在需要管理数千个HashMap实例时考虑 HashMaps HashMap的内存占用

Full example