如何修改哈希集中不属于哈希计算的属性?



我有一个结构,其中包含一个唯一的id,并将该ID用于其哈希:

use std::borrow::Borrow;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
type Id = u32;
#[derive(Debug, Eq)]
struct Foo {
id: Id,
other_data: u32,
}
impl PartialEq for Foo {
fn eq(&self, other: &Foo) -> bool {
self.id == other.id
}
}
impl Hash for Foo {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Borrow<Id> for Foo {
fn borrow(&self) -> &Id {
&self.id
}
}

我知道一旦我将其放入HashSet中,我就无法修改Foo::id的值,因为这会更改哈希。但是,我想修改Foo::other_data.我知道我可以将其从HashSet中删除,修改它,然后再次插入,但是像get_mut()这样的方法会干净得多。有没有办法完成这样的事情:

fn main() {
let mut baz = HashSet::new();
baz.insert(Foo {
id: 1,
other_data: 2,
});
if let Some(x) = baz.get_mut(&1) {
*x = 3;
}
}

这是一种反模式吗;我应该改用HashMap吗?

与这个问题有关。

这在您当前的数据结构中是不可能的。

HashSet故意不提供改变值的方法。正如您所提到的,在大多数情况下,改变HashSet中的值(或HashMap中的键)将使哈希无效。该 API 鼓励正确使用,甚至提到这一点:

修改

项目时,项目哈希值(由Hash特征确定)或其相等性(由Eq特征确定)在集合中时发生更改,这是一种逻辑错误。这通常只能通过CellRefCell、全局状态、I/O 或不安全代码来实现。

这暗示了你可以通过使用内部可变性来解决问题的一种方法:

use std::cell::Cell;
#[derive(Debug, Eq)]
struct Foo {
id: Id,
other_data: Cell<u32>,
}
fn main() {
let mut baz = HashSet::new();
baz.insert(Foo {
id: 1,
other_data: Cell::new(2),
});
if let Some(x) = baz.get(&1) {
x.other_data.set(3);
}
}

这是一件合理的事情,但我不会为此感到兴奋。相反,我将允许我的类型分解为键和值,并将其存储在HashMap中,如前所述。类似的东西


impl Foo {
// or insert_into_hashmap(self, &mut HashMap<Id, u32>)
fn into_key_value(self) -> (Id, u32) {
(self.id, self.other_data)
}
// Maybe a
//
// fn from_key_value(&'a Id, &'a u32) -> Self
// or
// fn from_hashmap(Id, &HashMap<Id, u32>) -> Self
}
// Maybe a
//
// struct FooRef<'a> { (or FooRefMut?) 
//     id: &'a Id,
//     other_data: &'a u32,
// }
//
// With a
// fn from_key_value(&'a Id, &'a u32) -> Self
// or
// fn from_hashmap(Id, &HashMap<Id, u32>) -> Self
fn main() {
let mut baz = HashMap::new();
let f = Foo {
id: 1,
other_data: 2,
};
let (k, v) = f.into_key_value();
baz.insert(k, v);
// See also HashMap::get_key_value
if let Some(v) = baz.get_mut(&1) {
*v = 3;
}
}

我相信在这种情况下unsafe代码是最好的途径。

impl Foo {
fn set_other_data(set: &mut HashSet<Foo>, id: &Id, data: u32) -> bool{
match set.get(id) {
Some(x) => {
let p: *const Foo = x;
let q: *mut Foo = p as *mut Foo;
unsafe {
(*q).other_data = data;
}
return true;
}
None => return false,
}
}
}
fn main() {
let mut baz = HashSet::new();
baz.insert(Foo {
id: 1,
other_data: 2,
});
Foo::set_other_data(&mut baz, &1, 3);
assert_eq!(3, baz.get(&1).unwrap().other_data);
}

正如Shepmaster引用的那样:

修改项目的方式是,项目的

哈希值(由Hash特征确定)或其相等性(由Eq特征确定)在集合中时发生更改,这是一种逻辑错误。这通常只能通过CellRefCell、全局状态、I/O 或不安全代码来实现。

在这种情况下,HashEq性状不使用other_data。所以它可以安全地发生突变。最大的危险是,在以后的某个时间点,Hash for FooEq for Foo将被修改并包括other_data

不存在数据竞争的危险,因为HashSet<Foo>是可变借用的。

其他选项:

分解:当Foo只有 2 个元素,但假设Foo包含许多元素时,这有效。您是将Foo分解为其所有单个元素(看起来很混乱)还是在Foo中创建子结构(代码膨胀)。

封装:Silvio Mayolo建议将Foo封装在内部使用HashMap的类似HashSet的接口中。这样可以保持 API 干净并仅使用safe代码,但似乎需要进行更多编程。

感谢您的反馈,如果这看起来合理,我可以提出功能请求,以便为HashSet提供unsafe fn get_mut()

最新更新