У меня есть черта со связанным типом Value и два метода. Один возвращает &Self::Value, а другой получает Self::Value. Эта черта реализована для нескольких типов, но я хочу реализовать ее для одного типа, в частности, где type Value = Option<...>. Это создает проблему, как показано ниже (игровая площадка):
pub trait PersistedContainer {
type Value;
fn get_persisted(&self) -> &Self::Value;
fn set_persisted(&mut self, value: Self::Value);
}
pub struct SelectState<Item> {
items: Vec<Item>,
selected: Option<usize>,
}
impl<Item: PartialEq> PersistedContainer for SelectState<Item> {
type Value = Option<Item>;
fn get_persisted(&self) -> &Self::Value {
self.selected.map(|index| &self.items[index])
}
fn set_persisted(&mut self, value: Self::Value) {
if let Some(value) = &value {
self.selected = self.items.iter().position(|item| item == value);
}
}
}
Компиляция приводит к этой ошибке:
error[E0308]: mismatched types
--> src/lib.rs:18:9
|
17 | fn get_persisted(&self) -> &Self::Value {
| ------------ expected `&Option<Item>` because of return type
18 | self.selected.map(|index| &self.items[index])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&Option<Item>`, found `Option<&Item>`
|
= note: expected reference `&Option<_>`
found enum `Option<&_>`
В идеале мы хотим вернуть Option<&Item> из get_persisted, но сигнатура признака заставляет нас вместо этого вернуться &Option<Item>. Проблема в том, что я не могу создать &Option<Item>, потому что у меня его нигде нет.
Некоторые решения, которые я рассмотрел:
impl<Item: Clone + PartialEq> PersistedContainer for SelectState<Item> {
type Value = Option<Item>;
fn get_persisted(&self) -> &Self::Value {
&self.selected.map(|index| self.items[index].clone())
}
fn set_persisted(&mut self, value: Self::Value) {
if let Some(value) = &value {
self.selected = self.items.iter().position(|item| item == value);
}
}
}
Это не работает, поскольку он пытается вернуть ссылку на временное значение. В любом случае я бы хотел избежать клона.
error[E0515]: cannot return reference to temporary value
--> src/lib.rs:18:9
|
18 | &self.selected.map(|index| self.items[index].clone())
| ^----------------------------------------------------
| ||
| |temporary value created here
| returns a reference to data owned by the current function
Вместо того, чтобы возвращать Option<Item>, пусть &Self::Value будет ссылкой для начала, например:
pub trait PersistedContainer {
type Value<'this> where Self: 'this;
fn get_persisted(&self) -> Self::Value<'_>;
fn set_persisted(&mut self, value: Self::Value<'_>);
}
pub struct SelectState<Item> {
items: Vec<Item>,
selected: Option<usize>,
}
impl<Item: Clone + PartialEq> PersistedContainer for SelectState<Item> {
type Value<'this> = Option<&'this Item> where Self: 'this;
fn get_persisted(&self) -> Self::Value<'_> {
self.selected.map(|index| &self.items[index])
}
fn set_persisted(&mut self, value: Self::Value<'_>) {
if let Some(value) = value {
self.selected = self.items.iter().position(|item| item == value);
}
}
}
Это компилируется, но проблема в том, что Self::Value теперь получает set_persisted вместо Option<&Item>. Вызывающий Option<Item> имеет собственную версию set_persisted и не может создать эталонную версию. Это приводит нас к следующему решению:
pub trait PersistedContainer {
type GetValue<'this> where Self: 'this;
type SetValue;
fn get_persisted(&self) -> Self::GetValue<'_>;
fn set_persisted(&mut self, value: Self::SetValue);
}
pub struct SelectState<Item> {
items: Vec<Item>,
selected: Option<usize>,
}
impl<Item: Clone + PartialEq> PersistedContainer for SelectState<Item> {
type GetValue<'this> = Option<&'this Item> where Self: 'this;
type SetValue = Option<Item>;
fn get_persisted(&self) -> Self::GetValue<'_> {
self.selected.map(|index| &self.items[index])
}
fn set_persisted(&mut self, value: Self::SetValue) {
if let Some(value) = &value {
self.selected = self.items.iter().position(|item| item == value);
}
}
}
Это работает, но требует от разработчиков дважды указывать один и тот же тип. Поскольку это будет библиотека, я хочу, чтобы API был максимально чистым.
Я думаю, что мне нужна какая-то черта, которая сопоставляет Self::Value -> T и &T с Option<T>, но я ничего об этом не нашел в Интернете. Я что-то упускаю или это просто не может быть выражено в системе типов?

&Option<Item> таким методом невозможно.К сожалению, &Option<Item> не является своего рода значением, которое этот тип метода может разумно вернуть, и причина в том, что это связано со структурой памяти вашей программы и с тем, что на самом деле представляет собой ссылка.
Ваш контейнер хранит ваши значения Item в файле Vec. Таким образом, в зависимости от типа Item, Vec может хранить значения Item плотно упакованными, без места между ними. Например, u64 — это одна из возможностей для Item, которая будет храниться следующим образом: первый u64 займет первые 8 байтов выделения вектора, второй u64 займет следующие 8 байтов и так далее. С методами, которые возвращают ссылки с тем же временем жизни, что и self (например, get_persisted), метод возвращает ссылку непосредственно в соответствующую часть Vec. Например, предположим, что вы писали impl для другого типа, который только что вернул &Item; ссылка может указывать прямо на &Item внутри вектора.
Существует несколько различных способов реализации Option<Item> в Rust, в зависимости от типа Item. Однако для некоторых типов реализация Option<Item> осуществляется с использованием префикса, который помещается рядом со значением. Например, Option<u64> внутренне реализован с использованием структуры, которая, если бы она была реализована непосредственно в Rust, а не внутри компилятора, выглядела бы примерно так:
struct OptionU64 {
is_some: bool,
value: MaybeUninit<u64> // initialised if and only if is_some
}
Причина, по которой у вас возникли проблемы с возвратом &Option<Item>, заключается в том, что для возврата значения Some компилятору придется найти блок памяти, в котором в нужном месте памяти есть маркер «это значение является Some». относительно фактического значения и вернуть ссылку на него – но поскольку элементы Vec плотно упакованы в памяти, это невозможно, поскольку память непосредственно перед значением представляет собой другое значение, а не дополнительную информацию, которая нужна Option чтобы указать, является ли значением Some или None. Тот факт, что значение item существует в памяти, не означает, что значение Some(item) существует в памяти (обычно, чтобы создать значение Some, вам нужно переместить его в Option).
(Кстати, это можно было бы реализовать неэффективно, храня массив Vec<Option<Item>>, все элементы которого были Some – это означало бы, что Option<Item> действительно существовал бы в памяти, на который вы могли бы вернуть ссылку. Но это бы означало, что Item действительно существовал бы в памяти, на который вы могли бы вернуть ссылку. это плохая идея, потому что для многих вариантов T это может удвоить размер вектора.)
&T в Option<T>, а Option<&T> в T.Такое преобразование не может работать для всех значений T: если вы принимаете Option<U> за Option<U>, то ему нужно будет перевести &Option<U> в Option<T>, что противоречит предположению, что оно преобразует Option<&T> в Result.
Если у вас есть метод типажа, который должен иметь возможность возвращать ошибку для некоторых типов, но всегда завершается успешно, когда используются другие типы, это можно сделать с помощью SelectState со связанным типом для случая ошибки:
pub trait PersistedContainer {
type Value;
type Error;
fn try_get_persisted(&self) -> Result<&Self::Value, Self::Error>;
fn set_persisted(&mut self, value: Self::Value);
}
Чтобы реализовать это на struct, наиболее распространенным подходом в библиотеках было бы создание тривиального Error, обозначающего сбой, и использование его в качестве возврата try_get_persisted:
// implement whatever traits on `NotSelected` seem reasonable
pub struct NotSelected;
impl<Item: PartialEq> PersistedContainer for SelectState<Item> {
type Value = Item;
type Error = NotSelected;
fn try_get_persisted(&self) -> Result<&Self::Value, Self::Error> {
match self.selected {
Some(index) => Ok(&self.items[index]),
None => Err(NotSelected)
}
}
fn set_persisted(&mut self, value: Self::Value) {
self.selected = self.items.iter().position(|item| *item == value);
}
}
Обратите внимание, что такой подход, помимо четкого set_persisted, также упрощает value — теперь ему всегда передается истинный Some, поэтому пользователям вашей библиотеки не нужно каждый раз заключать его в std::convert::Infallible.
Для реализаций, которые всегда успешны, а не имеют случай ошибки, вы можете использовать core::convert::Infallible (или эквивалентно #[no_std] при написании библиотеки PersistedContainer). Например, вот как можно реализовать Box на TryFrom:
impl<Item> PersistedContainer for Box<Item> {
type Value = Item;
type Error = std::convert::Infallible;
fn try_get_persisted(&self) -> Result<&Self::Value, Self::Error> {
Ok(&*self)
}
fn set_persisted(&mut self, value: Self::Value) {
**self = value;
}
}
Этот механизм используется стандартной библиотекой в таких фундаментальных функциях, как get_persisted. К сожалению, он еще не реализован полностью; try_get_persisted(…).into_ok() (для типов, которые его поддерживают) в конечном итоге должно быть выражено как .into_ok(), но Infallible нестабильно и плохо работает с текущим определением GetPersisted. Поэтому, чтобы работать с текущей версией Rust, вам, вероятно, придется реализовать ее самостоятельно. Вот как выглядит реализация try_get_persisted (которая должна быть вынесена в отдельный признак, потому что, хотя все сохраняемые контейнеры поддерживают get_persisted, не все поддерживают безошибочный match):
pub trait GetPersisted {
type Value;
fn get_persisted(&self) -> &Self::Value;
}
impl<Container: PersistedContainer<Error=std::convert::Infallible>> GetPersisted for Container {
type Value = <Container as PersistedContainer>::Value;
fn get_persisted(&self) -> &Self::Value {
match self.try_get_persisted() {
Ok(v) => v,
Err(err) => match err {}
}
}
}
Мои выводы таковы: хотя написать такие вещи правильно можно, Rust пока не справляется с задачей упростить это. К счастью, это уже признано проблемой и ведется работа по ее устранению (например, блок Result<T, Infallible>, который я написал выше для преобразования T в PersistedContainer, действительно должен быть частью стандартной библиотеки, и на самом деле в настоящее время есть планы добавить что-то подобное в стандартной библиотеке); просто реализация еще не находится в пригодном для использования состоянии.
Одна вещь, которая меня поражает T, это то, что вы, по сути, создали черту, которой нужны два разных API. В большинстве типов ваш признак хранит &T и получает SelectState — это то, от чего общий код мог бы разумно абстрагироваться. Однако в T ваш код (в том виде, в котором вы пытались его написать) хранит Option<T>, но требует, чтобы аргумент был указан как Some, который всегда равен Option<&T>, и получает PersistedContainer. Это два существенно разных API, и это, скорее всего, будет означать, что будет невозможно написать код, который в целом работал бы с любым PersistedContainer.
Если два разных API приемлемы для вашего варианта использования, то нет особого смысла пытаться объединить оба типа в один признак — вы могли бы также использовать get_persisted только в тех случаях, когда SelectState безошибочен, и писать код для его использования. SelectState непосредственно в тех случаях, когда вам нужен его API. (В конце концов, ваш код должен знать, имеет ли он дело с Option или нет, чтобы знать, нужно ли ему обрабатывать обертку PersistedContainer или нет.)
Если два разных API неприемлемы (т. е. код, использующий этот признак, должен иметь возможность обрабатывать все Result одинаково), вам придется использовать технику PersistedContainer из предыдущего раздела, чтобы создать что-то, что действительно работает. в общем, но это столкнется с упомянутыми выше проблемами, из-за которых соответствующие части стандартной библиотеки еще не полностью стабильны (или даже полностью реализованы).
Также вполне возможно, что вам вообще не нужен трейт — если нет смысла писать общий код, работающий с разными типами get_persisted, то использование трейта бесполезно и будет просто склонны запутывать дело. Вместо этого вы можете просто дать каждому типу внутреннюю реализацию set_persisted и Option::map, определенную таким образом, который имеет наибольший смысл для этого типа; повторяющиеся имена облегчат пользователю поиск правильного метода, и это, вероятно, единственная связь между нужными вам методами. (В качестве примера из стандартной библиотеки, Result::map и Option явно в некотором смысле являются одной и той же функцией, применяемой к двум разным типам, и их имена одинаковы, чтобы сделать связь более ясной, но они не принадлежат ни к какому виду черта, и вы не можете написать код, который может в общих чертах отображаться либо на Result, либо на &Option<Item>.)
Спасибо за очень подробный ответ. Я подозревал, что то, что я хотел, невозможно, но вы очень ясно объяснили, почему. Очевидно, это минимальный пример, позволяющий прояснить вопрос, но в полном коде обоснование признака становится ясным; существует произвольное количество реализаций PersistedContainer и одна конкретная структура, которая может вызывать любую реализацию через дженерики. Я воспользуюсь вашим try_get_persisted предложением. Еще раз спасибо!
Честно говоря, если одна реализация может не иметь значения, которое нужно получить, я бы предложил сделать интерфейс типажа уязвимым:
getвозвращаетOptionили дажеResult, если вам нужен фиксированный или связанныйErrorтип.