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);
}
}
答案 0 :(得分:23)
这当然是可能的。 signature of 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
不需要是一个简单的指针,它可以是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")));
}
说明:
我们介绍了Pair
和BorrowedPair
类型。由于the orphan rule E0210,我们无法直接使用(A, B)
。这很好,因为地图是一个实现细节。
KeyPair
特征扮演我们上面提到的Q
角色。我们需要impl Eq + Hash for KeyPair
,但Eq
和Hash
都不是object safe。我们添加了a()
和b()
方法来帮助您手动实施这些方法。
现在我们实施Borrow
从Pair<A, B>
到KeyPair + 'a
的特征。请注意'a
- 这是使Table::get
实际工作所需的微妙位。任意'a
允许我们说可以将Pair<A, B>
借用到任何生命周期的特征对象。如果我们未指定'a
,则未确定的特征对象将default to 'static
,这意味着仅当Borrow
之类的实现超过BorrowedPair
时才会应用'static
特征当然不是这样的。
最后,我们实施了Eq
和Hash
。如上所述,我们实施KeyPair + 'a
而不是KeyPair
(在此上下文中表示KeyPair + 'static
)。
在计算哈希值并检查get()
中的相等性时,使用特征对象会产生间接成本。如果优化器能够将其虚拟化,则可以消除成本,但LLVM是否会执行此操作尚不清楚。
另一种方法是将地图存储为HashMap<(Cow<A>, Cow<B>), f64>
。使用它需要较少的“聪明代码”,但现在存储内存成本以存储自有/借用标志以及get()
和set()
中的运行时成本。
除非您使用标准HashMap
进行分叉并添加方法仅通过Hash + Eq
查找条目,否则无法保证零成本解决方案。
答案 1 :(得分:5)
在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);
}
答案 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的内存占用。