Как избежать дублирования функций доступа для изменяемых и неизменных ссылок в Rust?
Несколько раз, я столкнулся с сценарием, где необходим метод accessor как для изменяемых, так и для неизменяемых ссылок.
Для ~ 3 строк не проблема дублировать логику, но когда логика становится более сложной, неплохо скопировать большие блоки кода.
Я хотел бы иметь возможность повторно использовать код для обоих.
Предоставляет ли Rust некоторый способ справиться с этим лучше, чем скопировать код или использовать unsafe
casts?
например:.
impl MyStruct {
pub fn get_foo(&self) -> &Bar {
// ~20 lines of code
// --- snip ---
return bar;
}
pub fn get_foo_mut(&mut self) -> &mut Bar {
// ~20 lines of code
// (exactly matching previous code except `bar` is mutable)
// --- snip ---
return bar;
}
}
Вот более подробная выдержка из базы кода, где неизменяемый возвращаемый аргумент был изменен на mutable для поддержки как неизменяемых, так и изменяемых версий функции. Это использует тип обернутого указателя (ConstP
и MutP
для неизменяемых и изменяемых ссылок), но логика функции должна быть ясной.
pub fn face_vert_share_loop<V, F>(f: F, v: V) -> LoopConstP
where V: Into<VertConstP>,
F: Into<FaceConstP>
{
into_expand!(f, v);
let l_first = f.l_first.as_const();
let mut l_iter = l_first;
loop {
if l_iter.v == v {
return l_iter;
}
l_iter = l_iter.next.as_const();
if l_iter == l_first {
break;
}
}
return null_const();
}
pub fn face_vert_share_loop_mut(f: FaceMutP, v: VertMutP) -> LoopMutP {
let l = face_vert_share_loop(f, v);
return unsafe {
// Evil! but what are the alternatives?
// Perform an unsafe `const` to `mut` cast :(
// While in general this should be avoided,
// its 'OK' in this case since input is also mutable.
l.as_mut()
};
}
Ответы
Ответ 1
На самом деле вы этого не делаете. Напомним, что T
, &T
и &mut T
- все разные типы. В этом контексте ваш вопрос совпадает с вопросом "Как избежать дублирования функций доступа для String
и HashMap
".
Matthieu M имел правильные термины "абстрактные над изменчивостью":
TL; DR - это то, что Rust, вероятно, нужно будет улучшить с помощью новых функций для поддержки этого. Поскольку никто не преуспел, никто не на 100% уверен, какие функции должны быть. В настоящее время лучше всего предположить, что более высокие типы типов (HKT).
Ответ 2
(ссылки на игровые площадки для решений с использованием параметры типа и связанные типы)
В этом случае &T
и &mut T
- всего два разных типа. Код, который является общим для разных типов (как во время компиляции, так и во время выполнения), идиоматически написан в Rust, используя черты. Например, данный:
struct Foo { value: i32 }
struct Bar { foo: Foo }
предположим, что мы хотим предоставить Bar
общий аксессор для своего члена данных Foo
. Аксессор должен работать как с &Bar
, так и с &mut Bar
, соответствующим образом возвращающим &Foo
или &mut Foo
. Поэтому мы пишем черту FooGetter
trait FooGetter {
type Output;
fn get(self) -> Self::Output;
}
задача которого должна быть общей для конкретного типа Bar
. Его тип Output
будет зависеть от Bar
, так как мы хотим get
иногда возвращать &Foo
, а иногда &mut Foo
. Обратите также внимание на то, что он потребляет self
типа self
. Поскольку мы хотим, чтобы get
был общим для &Bar
и &mut Bar
, нам нужно реализовать FooGetter
для обоих, так что self
имеет соответствующие типы:
// FooGetter::Self == &Bar
impl<'a> FooGetter for &'a Bar {
type Output = &'a Foo;
fn get(self) -> Self::Output { & self.foo }
}
// FooGetter::Self == &mut Bar
impl<'a> FooGetter for &'a mut Bar {
type Output = &'a mut Foo;
fn get(mut self) -> Self::Output { &mut self.foo }
}
Теперь мы можем легко использовать .get()
в общем коде, чтобы получить &
или &mut
ссылки на Foo
из &Bar
или &mut Bar
(просто требуя T: FooGetter
). Например:
// exemplary generic function:
fn foo<T: FooGetter>(t: T) -> <T as FooGetter>::Output {
t.get()
}
fn main() {
let x = Bar { foo: Foo {value: 2} };
let mut y = Bar { foo: Foo {value: 2} };
foo(&mut y).value = 3;
println!("{} {}\n", foo(&x).value, foo(&mut y).value);
}
Обратите внимание, что вы также можете реализовать FooGetter
для Bar
, так что get
является общим по &T
, &mut T
и T
сам (путем перемещения его). Это на самом деле метод .iter()
реализован в стандартной библиотеке и почему он всегда делает "правильную вещь" независимо от ссылочного аргумента, на который он ссылается.
Ответ 3
В настоящее время Rust не поддерживает абстрагирование над изменчивостью.
Есть некоторые способы, которыми это может быть достигнуто, хотя они не идеальны:
- Используйте макрос для разворачивания дубликата кода, объявления макроса и совместного использования между обеими функциями - необходимо сконструировать, чтобы он работал для изменяемого и неизменяемого, конечно.
- Запишите неизменяемую версию функции (чтобы ничего не изменилось), затем напишите функцию-оболочку для изменяемой версии, которая выполняет преобразование
unsafe
на результат, чтобы сделать его изменяемым.
Ни один из них не очень привлекателен (макрос слишком многословный и немного менее читабельный, добавляет некоторый код-раздувание), unsafe
более читабельна, но было бы неплохо избежать, поскольку приведение из неизменяемого в mutable isn 't так приятно иметь через базу кода.
На данный момент лучший вариант, насколько я могу видеть (где код для копирования не подходит), заключается в том, чтобы написать неизменяемую версию функции, а затем обернуть ее версией mut
функции, в которой оба входы и выходы изменяемы.
Для этого требуется выражение unsafe
на выходе функции, поэтому оно не идеально.
Примечание: важно, чтобы неизменяемая функция содержала тело кода, так как обратное позволит случайным образом изменить то, что может быть неизменным.