Индексация из HashMap с &str

Вопрос или проблема

Как правильно индексировать HashMap<&String, V> с помощью &str? Rust сообщает, что &String не является Borrow<str>, что требуется для индексации. (Это кажется мне немного нелепым; если T: Borrow<U>, то, конечно, &T: Borrow<U> также должно выполняться?)

use std::collections::HashMap;

fn main() {
    let strings = vec!["x".to_owned(), "y".to_owned(), "z".to_owned()];
    let m = [0, 1, 2]
        .into_iter()
        .map(|i| (&strings[i], i))
        .collect::<HashMap<&String, usize>>();
    let zero = m["x"];
}
error[E0277]: условие трейта `&String: Borrow<str>` не выполнено
 --> src/main.rs:9:18
  |
9 |     let zero = m["x"];
  |                  ^^^ трейт `Borrow<str>` не реализован для `&String`, что требуется для `HashMap<&String, usize>: Index<&_>`
  |
  = помощь: трейт `Borrow<str>` реализован для `String`
  = помощь: для этой реализации трейта ожидался `String`, найден `&String`
  = заметка: требуется для реализации `HashMap<&String, usize>` для `Index<&str>`

Очевидно, поскольку я сам создал этот конкретный HashMap, я мог бы вернуться и изменить его тип ключа на &str. Но предположим, что мне передали HashMap<&String, V>, как правильно его индексировать? Создание целого String, чтобы получить к нему ссылку, кажется расточительным.

impl<U, T: Borrow<U>> Borrow<U> for &T {

конфликтует с существующим

impl<T> Borrow<T> for T {

поэтому его нельзя добавить.

Playground

Когда вы контролируете ситуацию, не следует использовать &String в качестве ключей с самого начала. Предпочтительный способ – использовать &str, который можно легко создать из &String, и вы не столкнетесь с этой проблемой, а также это более универсально.

Когда вы не контролируете ситуацию, создание String лишь для индексации – ваш единственный вариант.

Существует способ обойти это, если вы готовы рискнуть.

Это решение использует тот факт, что если у вас есть структура S, отмеченная #[repr(transparent)] и содержащая единственное поле типа T, вы можете безопасно преобразовать &'a T в &'b S, где 'a: 'b. (Вот почему вы можете получить заем &Path из PathBuf, даже несмотря на то, что ни один Path никогда на самом деле не существует, например.)

Итак, давайте создадим новый тип, который мы можем заимствовать из String, и с позитивно обсуждаемым именем.

#[derive(Hash, PartialEq, Eq)]
#[repr(transparent)]
struct StrRef(str);

Это определение позволяет нам безопасно преобразовать &str в &StrRef, поэтому мы завернем эту часть небезопасного кода в реализацию From:

impl<'a> From<&'a str> for &'a StrRef {
    fn from(value: &'a str) -> &'a StrRef {
        // БЕЗОПАСНОСТЬ: StrRef имеет непрозрачный формат и содержит только str.
        unsafe { &*(value as *const str as *const StrRef) }
    }
}

Теперь мы можем реализовать Borrow<StrRef> для любого типа, из которого мы можем получить &str.

// На самом деле не нужно, просто для примера.
impl Borrow<StrRef> for String {
    fn borrow(&self) -> &StrRef {
        self.as_str().into()
    }
}

// Это реализация, которая вам нужна в данном случае.
impl Borrow<StrRef> for &String {
    fn borrow(&self) -> &StrRef {
        self.as_str().into()
    }
}

Наконец, это позволяет нам написать следующую функцию, которая достигает вашей цели получения значения из карты с использованием &str без его преобразования в принадлежащее String:

fn get_from_map<'a, T>(map: &'a HashMap<&String, T>, key: &str) -> Option<&'a T> {
    map.get(<&StrRef>::from(key))
}

(Playground)

Очевидно, если вы можете изменить тип ключа карты на &str, то вам следует предпочесть это, но если вы не контролируете источник карты, то это является жизнеспособным обходным решением (в – надеюсь – очень ограниченной области вашего приложения).

Ответ или решение

Для индексации в HashMap<&String, V> с помощью &str, следует учитывать, что &String не реализует трейт Borrow<str>, который необходим для такого вида индексации. Данный аспект языка Rust может показаться нелогичным, но он обусловлен тем, что &String и String — это разные типы, и даже если все строки могут быть представлены как &str, необходимо использовать соответствующий тип.

Если у вас есть доступ и возможность изменить структуру HashMap, лучшим выбором будет использование HashMap<&str, V>. Это позволит избежать данных проблем, однако, если вы не контролируете тип ключа (например, если он уже задан как &String), то следует рассмотреть альтернативные подходы.

Вот несколько способов, как можно решить проблему индексации:

  1. Создание нового String: Вы можете создать новый String из &str и использовать его для индексации, хотя это может быть неэффективно:

    let value = m.get(&key.to_string());

    Однако такой подход требует выделения памяти для нового объекта, что может быть неэффективно.

  2. Использование StrRef: Более продвинутый подход (и менее безопасный с точки зрения рисков) заключается в создании нового типа StrRef, который будет безопасно преобразовывать &str в &StrRef, как показано ниже:

    #[derive(Hash, PartialEq, Eq)]
    #[repr(transparent)]
    struct StrRef(str);
    
    impl<'a> From<&'a str> for &'a StrRef {
        fn from(value: &'a str) -> &'a StrRef {
            // SAFETY: StrRef is repr(transparent) and contains only a str.
            unsafe { &*(value as *const str as *const StrRef) }
        }
    }
    
    impl Borrow<StrRef> for &String {
        fn borrow(&self) -> &StrRef {
            self.as_str().into()
        }
    }
    
    fn get_from_map<'a, T>(map: &'a HashMap<&String, T>, key: &str) -> Option<&'a T> {
        map.get(&StrRef::from(key))
    }

    Данный код позволяет вам получить элемент из HashMap<&String, T> с использованием &str без необходимости создания нового String объекта.

  3. Безопасность с использованием unsafe: Использование unsafe может быть оправданным в определённых случаях, если вы принимаете на себя ответственность за его правильное использование. Важно помнить, что при таком подходе вы должны быть уверены в том, что доступны свойства repr(transparent) у структуры StrRef.

Итак, если у вас есть возможность изменить ключи вашей карты, всегда предпочтительнее использовать &str. Если же вы оказались в ситуации, когда вы вынуждены работать с HashMap<&String, V> и индексация по &str необходима, можно использовать приведенные выше подходы для обоснованного решения.

Оцените материал
Добавить комментарий

Капча загружается...