Вопрос или проблема
Как правильно индексировать 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 {
поэтому его нельзя добавить.
Когда вы контролируете ситуацию, не следует использовать &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))
}
Очевидно, если вы можете изменить тип ключа карты на &str
, то вам следует предпочесть это, но если вы не контролируете источник карты, то это является жизнеспособным обходным решением (в – надеюсь – очень ограниченной области вашего приложения).
Ответ или решение
Для индексации в HashMap<&String, V>
с помощью &str
, следует учитывать, что &String
не реализует трейт Borrow<str>
, который необходим для такого вида индексации. Данный аспект языка Rust может показаться нелогичным, но он обусловлен тем, что &String
и String
— это разные типы, и даже если все строки могут быть представлены как &str
, необходимо использовать соответствующий тип.
Если у вас есть доступ и возможность изменить структуру HashMap
, лучшим выбором будет использование HashMap<&str, V>
. Это позволит избежать данных проблем, однако, если вы не контролируете тип ключа (например, если он уже задан как &String
), то следует рассмотреть альтернативные подходы.
Вот несколько способов, как можно решить проблему индексации:
-
Создание нового
String
: Вы можете создать новыйString
из&str
и использовать его для индексации, хотя это может быть неэффективно:let value = m.get(&key.to_string());
Однако такой подход требует выделения памяти для нового объекта, что может быть неэффективно.
-
Использование
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
объекта. -
Безопасность с использованием
unsafe
: Использованиеunsafe
может быть оправданным в определённых случаях, если вы принимаете на себя ответственность за его правильное использование. Важно помнить, что при таком подходе вы должны быть уверены в том, что доступны свойстваrepr(transparent)
у структурыStrRef
.
Итак, если у вас есть возможность изменить ключи вашей карты, всегда предпочтительнее использовать &str
. Если же вы оказались в ситуации, когда вы вынуждены работать с HashMap<&String, V>
и индексация по &str
необходима, можно использовать приведенные выше подходы для обоснованного решения.