[英]How to implement HashMap with two keys?
HashMap
實現了get
和insert
方法,它們分別采用一個不可變的借用和一個值的單個移動。
我想要一個像這樣但需要兩個鍵而不是一個的特征。 它使用內部的地圖,但它只是實現的一個細節。
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
類型,使得
(A, B): Borrow<Q>
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")));
}
解釋:
KeyPair
trait 扮演了我們上面提到的Q
的角色。 我們需要impl Eq + Hash for dyn KeyPair
,但Eq
和Hash
都不是對象安全的。 我們添加了a()
和b()
方法來幫助手動實現它們。
現在我們實現從(A, B)
到dyn KeyPair + 'a
的Borrow
trait。 注意'a
- 這是使Table::get
實際工作所需的一個微妙位。 任意'a
允許我們說(A, B)
可以在任何生命周期內借用給 trait 對象。 如果我們不指定'a
,則未指定大小的 trait 對象將默認為'static
,這意味着Borrow
trait 只能在(&A, &B)
等實現超過'static
時應用,當然不是這樣。
最后,我們實現Eq
和Hash
。 與第 2 點相同的原因,我們實現了dyn KeyPair + '_
而不是dyn KeyPair
(這意味着dyn KeyPair + 'static
在這種情況下)。 這里的'_
是一種語法糖,表示任意生命周期。
在計算哈希和檢查get()
中的相等性時,使用 trait 對象會產生間接成本。 如果優化器能夠將其去虛擬化,則可以消除成本,但 LLVM 是否會這樣做是未知的。
另一種方法是將地圖存儲為HashMap<(Cow<A>, Cow<B>), f64>
。 使用它需要更少的“聰明代碼”,但現在在get()
和set()
中存儲擁有/借用標志以及運行時成本都會產生內存成本。
除非您分叉標准HashMap
並添加一個方法來僅通過Hash + Eq
查找條目,否則沒有保證零成本的解決方案。
在get
方法中, a
和b
借用的值在內存中可能彼此不相鄰。
[--- A ---] other random stuff in between [--- B ---]
\ /
&a points to here &b points to here
借用類型&(A, B)
將需要相鄰的A
和B
[--- 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.