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

问题描述 投票:0回答:2

我有一个包含唯一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
    }
}

我知道,一旦我将它放入Foo::id,我就无法修改HashSet的值,因为这会改变哈希值。但是,我想修改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吗?

Related to this question.

rust hashset
2个回答
7
投票

您当前的数据结构无法做到这一点。

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

-2
投票

我相信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);
}

正如船长报价:

以这样的方式修改项目是一个逻辑错误:由Hash特征确定的项目散列,或由Eq特征确定的等式,在它在集合中时发生变化。这通常只能通过CellRefCell,全局状态,I / O或不安全代码实现。

在这种情况下,other_data不被HashEq特征使用。所以它可以安全地变异。最大的危险是,稍后Hash for FooEq for Foo将被修改并包括other_data

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

其他选择:

分解:这在Foo只有2个元素时起作用,但假设Foo包含许多元素。你是否将Foo分解为所有单独的元素(看起来很混乱)或者在Foo(代码膨胀)中创建子结构。

封装:Silvio Mayolo建议将Foo封装在内部使用HashSetHashMap界面中。这使得API保持干净并仅使用safe代码,但似乎需要更多编程。

我将非常感谢您的反馈,如果这看似合理,我可以为unsafe fn get_mut()提供HashSet的功能请求。

© www.soinside.com 2019 - 2024. All rights reserved.