HashMap
implémente les méthodes get
et insert
qui prennent un seul emprunt immuable et un seul mouvement d'une valeur, respectivement.
Je veux un trait qui est juste comme ça mais qui prend deux clés au lieu d'une. Il utilise la carte à l'intérieur, mais c'est juste un détail de mise en œuvre.
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);
}
}
C'est certainement possible. La signature de get
est
fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq,
Le problème ici est d’implémenter un type &Q
tel que
(A, B): Borrow<Q>
Q
implémente Hash + Eq
Pour satisfaire la condition (1), nous devons réfléchir à la manière d'écrire
fn borrow(self: &(A, B)) -> &Q
L'astuce est que &Q
n'a pas besoin d'être un simple pointeur , ce peut être un objet trait ! L'idée est de créer un trait Q
qui aura deux implémentations:
impl Q for (A, B)
impl Q for (&A, &B)
L'implémentation Borrow
renverra simplement self
et nous pouvons construire un objet trait &Q
à partir des deux éléments séparément.
Le implémentation complète est comme ceci:
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")));
}
Explication:
Nous avons introduit les types Pair
et BorrowedPair
. Nous ne pouvons pas utiliser (A, B)
directement à cause de la règle Orphan E0210 . Cela convient car la carte est un détail de la mise en œuvre.
Le trait KeyPair
prend le rôle de Q
mentionné ci-dessus. Nous aurions besoin de impl Eq + Hash for KeyPair
, mais Eq
et Hash
ne sont pas tous les deux object safe . Nous ajoutons les méthodes a()
et b()
pour faciliter leur implémentation manuelle.
Maintenant, nous implémentons le trait Borrow
de Pair<A, B>
à KeyPair + 'a
. Notez le 'a
- c’est un élément subtil qui est nécessaire pour que Table::get
fonctionne réellement. Le 'a
arbitraire nous permet de dire qu'un Pair<A, B>
peut être emprunté à l'objet trait pour any cycle de vie. Si nous ne spécifions pas le 'a
, l'objet trait non dimensionné sera par défaut à 'static
, ce qui signifie que le trait Borrow
ne peut être appliqué que si une implémentation telle que BorrowedPair
survit à 'static
, ce qui n'est certainement pas le cas.
Enfin, nous implémentons Eq
et Hash
. Comme ci-dessus, nous implémentons pour KeyPair + 'a
au lieu de KeyPair
(ce qui signifie KeyPair + 'static
dans ce contexte).
L'utilisation d'objets trait générera des coûts indirectionnels lors du calcul du hachage et de la vérification de l'égalité dans get()
. Le coût peut être éliminé si l'optimiseur est capable de le devirtualiser, mais on ignore si LLVM le fera.
Une alternative consiste à stocker la carte sous la forme HashMap<(Cow<A>, Cow<B>), f64>
. L'utilisation de cette méthode nécessite moins de "code intelligent", mais il y a maintenant un coût en mémoire pour stocker l'indicateur de propriété/emprunté ainsi que le coût d'exécution à la fois dans get()
et set()
.
À moins que vous n'utilisiez la variable HashMap
standard et que vous ajoutiez une méthode pour rechercher une entrée uniquement via Hash + Eq
, il n'existe aucune solution à coût zéro.
Dans la méthode get
, les valeurs empruntées par a
et b
peuvent ne pas être adjacentes en mémoire.
[--- A ---] other random stuff in between [--- B ---]
\ /
&a points to here &b points to here
Emprunter une valeur de type &(A, B)
nécessiterait une A
et une B
adjacentes.
[--- A ---][--- B ---]
\
we could have a borrow of type &(A, B) pointing to here
Un peu de code non sécurisé peut résoudre ce problème! Nous avons besoin d’une copie superficielle de *a
et *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);
}
Un trait Memory
qui prend deux clés, set par valeur et get par référence:
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);
}
Vous pouvez impl
un tel trait en utilisant une carte de cartes:
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);
}
}
Veuillez noter que si la solution est assez élégante, l’empreinte mémoire d’un HashMap de HashMaps doit être prise en compte lorsque des milliers d’instances HashMap
doivent être gérées.