Как я могу изменить свойства в HashSet, которые не являются частью вычисления хеша? - PullRequest
0 голосов
/ 02 апреля 2019

У меня есть структура, которая содержит уникальный идентификатор и использует этот идентификатор для своего хэша:

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 вместо этого?

С этим вопросом связано.

Ответы [ 2 ]

7 голосов
/ 02 апреля 2019

Это невозможно с вашей текущей структурой данных.

HashSet преднамеренно не предоставляет методы для изменения значений.Как вы уже упоминали, изменение значения в HashSet (или ключ в HashMap) приведет к аннулированию хеша в большинстве случаев.API поощряет правильное использование и даже упоминает об этом:

Логическая ошибка для элемента, модифицируемого таким образом, что хеш элемента, как определено * 1010Черта * или ее равенство, определяемое чертой Eq, изменяется, пока она находится в наборе.Обычно это возможно только через Cell, RefCell, глобальное состояние, ввод / вывод или небезопасный код.

Это относится к одномуТаким образом, вы можете решить свою проблему, используя внутреннюю изменчивость:

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;
    }
}
0 голосов
/ 03 апреля 2019

Я считаю, что 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, изменяется, пока он находится в наборе,Обычно это возможно только через Cell, RefCell, глобальное состояние, ввод / вывод или небезопасный код.

В этом случае other_data не используется чертами Hash или Eq.Так что его можно смело мутировать.Самая большая опасность состоит в том, что на более позднем этапе Hash for Foo или Eq for Foo будут изменены и включают в себя other_data.

Нет опасности гонок данных, поскольку HashSet<Foo> является заимствованным образом заимствованным.

Другие опции:

Разложение: работает, когда Foo имеет только 2 элемента, но предположим, что Foo содержит много элементов.Разлагаете ли вы Foo на все его отдельные элементы (кажется беспорядочным) или создаете подструктуры в Foo (раздувание кода).

Инкапсуляция: Сильвио Майоло предложил инкапсулировать Foo в HashSetкак интерфейс, который внутренне использует HashMap.Это поддерживает API в чистоте и использует только код safe, но кажется, что требуется больше программирования, чем необходимо.

Буду признателен за ваш отзыв, и если это будет разумным, я могу добавить запрос на добавление unsafe fn get_mut()для HashSet.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...