簡體   English   中英

如何用兩個鍵實現 HashMap?

[英]How to implement HashMap with two keys?

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

這當然是可能的。 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

為了滿足條件(1),我們需要考慮如何寫

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

訣竅是&Q不需要是一個簡單的指針,它可以是一個特征對象 這個想法是創建一個具有兩個實現的特征Q

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

Borrow實現將簡單地返回self ,我們可以分別從這兩個元素構造一個&dyn Q trait 對象。


完整的實現是這樣的:

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

// See explanation (1).
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 (2).
impl<'a, A, B> Borrow<dyn KeyPair<A, B> + 'a> for (A, B)
where
    A: Eq + Hash + 'a,
    B: Eq + Hash + 'a,
{
    fn borrow(&self) -> &(dyn KeyPair<A, B> + 'a) {
        self
    }
}

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

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

impl<A: Eq, B: Eq> Eq for dyn KeyPair<A, B> + '_ {}

// OP's Table struct
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 new() -> Self {
        Table {
            map: HashMap::new(),
        }
    }

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

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

// Boring stuff below.

impl<A, B> KeyPair<A, B> for (A, B) {
    fn a(&self) -> &A {
        &self.0
    }
    fn b(&self) -> &B {
        &self.1
    }
}
impl<A, B> KeyPair<A, B> for (&A, &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. KeyPair trait 扮演了我們上面提到的Q的角色。 我們需要impl Eq + Hash for dyn KeyPair ,但EqHash都不是對象安全的。 我們添加了a()b()方法來幫助手動實現它們。

  2. 現在我們實現從(A, B)dyn KeyPair + 'aBorrow trait。 注意'a - 這是使Table::get實際工作所需的一個微妙位。 任意'a允許我們說(A, B)可以在任何生命周期內借用給 trait 對象。 如果我們不指定'a ,則未指定大小的 trait 對象將默認為'static ,這意味着Borrow trait 只能在(&A, &B)等實現超過'static時應用,當然不是這樣。

  3. 最后,我們實現EqHash 與第 2 點相同的原因,我們實現了dyn KeyPair + '_而不是dyn KeyPair (這意味着dyn KeyPair + 'static在這種情況下)。 這里的'_是一種語法糖,表示任意生命周期。


在計算哈希和檢查get()中的相等性時,使用 trait 對象會產生間接成本。 如果優化器能夠將其去虛擬化,則可以消除成本,但 LLVM 是否會這樣做是未知的。

另一種方法是將地圖存儲為HashMap<(Cow<A>, Cow<B>), f64> 使用它需要更少的“聰明代碼”,但現在在get()set()中存儲擁有/借用標志以及運行時成本都會產生內存成本。

除非您分叉標准HashMap並添加一個方法來僅通過Hash + Eq查找條目,否則沒有保證零成本的解決方案。

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

一個Memory trait,它接受兩個鍵,按值設置並通過引用獲取

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

您可以使用 Map of Maps 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實例時,必須考慮HashMap 的 HashMap的內存占用。

完整示例

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM