【问题标题】:How to implement HashMap with two keys?如何用两个键实现 HashMap?
【发布时间】:2018-01-28 21:57:45
【问题描述】:

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

【问题讨论】:

  • 你遇到了什么问题?
  • 我无法理解如何借用两个借用并制作一个借用的元组。或者如何把&A和&B变成&(A,B),这就是我需要.gethashmap
  • 如果您展示您的尝试以及遇到的错误或问题,人们会更容易帮助您。见stackoverflow.com/help/mcve
  • 我不认为你可以,因为 (A,B) 元组要求它的元素在内存中彼此相邻,而你的 &amp;A&amp;B 可以存储在任何地方。您将需要使用 Copyable 或 Cloneable 键,以便您可以根据需要复制它们,或者删除引用并将它们移动到您的 get 函数中。
  • 借用检查器潜伏在你试图做的事情的背后:虽然你在AB 上分别借用了,但调用者和哈希图都不会保留元组的所有权(&amp;A, &amp;B)。如果您自己构建元组并使用对它的引用,则当元组被映射引用时,没有人持有所有权。长话短说:你不能。直接为&amp;(&amp;A, &amp;B) 构造映射并让调用者构造元组,而不是尝试使用两个单独的键。

标签: hashmap rust


【解决方案1】:

这当然是可能的。 signature of get

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

这里的问题是实现一个&amp;Q 类型,这样

  1. (A, B): Borrow&lt;Q&gt;
  2. Q 实现 Hash + Eq

要满足条件(1),我们需要考虑怎么写

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

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

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

Borrow 实现将简单地返回 self,我们可以分别从这两个元素构造一个 &amp;dyn Q 特征对象。


full implementation 是这样的:

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 都不是object safe。我们添加了a()b() 方法来帮助手动实现它们。

  2. 现在我们实现了从(A, B)dyn KeyPair + 'aBorrow 特征。请注意'a — 这是使Table::get 实际工作所需的一个微妙位。任意的'a 允许我们说(A, B) 可以在any 生命周期内借用给特征对象。如果我们不指定'a,则unsized trait 对象将default to 'static,这意味着Borrow trait 只能在(&amp;A, &amp;B) 这样的实现超过'static 时应用,当然不是这样。

  3. 最后,我们实现了EqHash。与第 2 点相同的原因,我们实现 dyn KeyPair + '_ 而不是 dyn KeyPair(在此上下文中表示 dyn KeyPair + 'static)。这里的'_ 是一个语法糖,意思是任意生命周期。


get() 中计算哈希和检查相等性时,使用 trait 对象会产生间接成本。如果优化器能够将其去虚拟化,则可以消除成本,但 LLVM 是否会这样做是未知的。

另一种方法是将地图存储为HashMap&lt;(Cow&lt;A&gt;, Cow&lt;B&gt;), f64&gt;。使用它需要更少的“智能代码”,但现在在 get()set() 中存储拥有/借用标志以及运行时成本都会产生内存成本。

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

【讨论】:

  • 我对这个答案的完整和详细程度感到震惊。在读了三遍之后(那是关于 Rust,而不是你),我想我明白了。是的,我以为我错过了一些东西,但事实证明这个问题有点棘手。非常感谢。
  • 是的,疯狂的道具。这是对这个问题的一个很好的回答,也是初学者如何利用和理解 Rust 特征系统的一个很好的例子
  • 有点令人困惑的是,第 14 行 (impl Borrow for (A, B)) 实际上涵盖了 (A, B)(&amp;A, &amp;B)。我试图将此解决方案移植到一个类似的问题,其中我使用struct Foo([T; 2]) 作为键类型,并且我需要分别为Foo(&amp;T, &amp;T) 实现借用。这实际上在 (2) 中进行了解释,但我一开始忽略了这一点,花了一段时间才注意到。
  • 另一个注意事项是,从 Rust 1.56.0-nightly (2021-07-31) 开始,如果你错过了 HashEq impl 后面的 + '_,你会得到第 53 行出现错误 explicit lifetime required in the type of b,用 lifetime 'static required 注释 self.map.get 而不指向正确的位置。这很令人困惑,因为我们没有在代码中的任何地方明确提及'static,但dyn 暗示了这一点。
【解决方案2】:

一个Memory trait,它有两个键,set by value 和 get by reference:

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 这样的 trait:

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 of HashMaps 的内存占用。

Full example

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-04-10
    • 2015-12-23
    • 2013-01-18
    • 2011-05-26
    • 2016-03-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多