reference - used - ¿Cómo evitar escribir funciones de acceso duplicadas para referencias mutables e inmutables en Rust?
tag use in html (3)
Algunas veces, me he encontrado con el escenario en el que se necesita un método de acceso para referencias mutables e inmutables.
Para ~ 3 líneas no es un problema duplicar la lógica, pero cuando la lógica se vuelve más compleja, no es bueno copiar y pegar grandes bloques de código.
Me gustaría poder reutilizar el código para ambos.
¿Rust proporciona alguna manera de manejar esto mejor que copiar y pegar código, o usar moldes
unsafe
?
p.ej:
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;
}
}
Aquí hay un extracto más detallado de una base de código donde un argumento de retorno inmutable se convirtió en mutable para admitir las versiones inmutable y mutable de una función.
Esto utiliza un tipo de puntero envuelto (
ConstP
y
MutP
para referencias inmutables y mutables), pero la lógica de la función debe ser clara.
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()
};
}
(enlaces de juegos a soluciones usando parámetros de tipo y tipos asociados )
En este caso,
&T
y
&mut T
son solo dos tipos diferentes.
El código que es genérico sobre diferentes tipos (tanto en tiempo de compilación como en tiempo de ejecución) se escribe idiomáticamente en Rust utilizando rasgos.
Por ejemplo, dado:
struct Foo { value: i32 }
struct Bar { foo: Foo }
supongamos que queremos proporcionarle a
Bar
un acceso genérico para su miembro de datos
Foo
.
El descriptor de acceso debe trabajar tanto en
&Bar
como en
&mut Bar
devolviendo adecuadamente
&Foo
o
&mut Foo
.
Entonces escribimos un rasgo
FooGetter
trait FooGetter {
type Output;
fn get(self) -> Self::Output;
}
cuyo trabajo es ser genérico sobre el tipo particular de
Bar
que tenemos.
Su tipo de
Output
dependerá de la
Bar
ya que queremos
get
a veces return
&Foo
y a veces
&mut Foo
.
Tenga en cuenta también que consume
self
del tipo
Self
.
Como queremos
get
a ser genéricos sobre
&Bar
y
&mut Bar
, necesitamos implementar
FooGetter
para ambos, de modo que
Self
tenga los tipos apropiados:
// 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 }
}
Ahora podemos usar fácilmente
.get()
en código genérico para obtener referencias de
&
o
&mut
a
Foo
desde una
&Bar
o una
&mut Bar
(simplemente requiriendo
T: FooGetter
).
Por ejemplo:
// 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);
}
Tenga en cuenta que también puede implementar
FooGetter
para
Bar
, de modo que
get
sea genérico sobre
&T
,
&mut T
y
T
(moviéndolo).
Así es como se implementa el método
.iter()
en la biblioteca estándar, y por qué siempre hace "lo correcto" independientemente de la referencia del argumento en el que se invoca.
Actualmente, Rust no admite abstracción sobre mutabilidad.
Hay algunas formas en que esto se puede lograr, aunque no son ideales:
- Use una macro para expandir el código duplicado, declare la macro y comparta entre ambas funciones; debe construirse de modo que funcione para mutable e inmutable, por supuesto.
-
Escriba la versión inmutable de la función (para asegurarse de que no se cambie nada), luego escriba una función de contenedor para la versión mutable que realice una
unsafe
en el resultado para hacerlo mutable.
Ninguno de estos es muy atractivo (una macro es demasiado detallada y un poco menos legible, agrega un poco de código hinchado), lo
unsafe
es más legible, pero sería bueno evitarlo, ya que la conversión de inmutable a mutable no es tan agradable tener a través de una base de código.
Por ahora, la mejor opción hasta donde puedo ver (donde el código de copiar y pegar no es aceptable), es escribir una versión inmutable de la función, luego envolverla con una versión
mut
de la función donde las entradas y salidas son mutables .
Esto requiere una
unsafe
en la salida de la función, por lo que no es ideal.
Nota: es importante que la función inmutable contenga el cuerpo del código, ya que lo contrario permitirá la mutación accidental de lo que podría ser una entrada inmutable.
No lo haces, de verdad.
Recuerde que
T
,
&T
y
&mut T
son
tipos diferentes
.
En ese contexto, su pregunta es la misma que preguntar "Cómo evitar escribir funciones de acceso duplicadas para
String
y
HashMap
".
Matthieu M tenía los términos correctos "resumen sobre la mutabilidad":
- Parametrización sobre mutabilidad
- Tratar con & / & mut en estructuras de datos: ¿resumen sobre mutabilidad o tipos divididos?
- ¿Una forma segura de reutilizar el mismo código para variantes inmutables y mutables de una función?
- Resumen sobre la mutabilidad en Rust
- "Polimorfismo de mutabilidad"
- etc. etc. etc.
El TL; DR es que Rust probablemente necesitaría ser mejorado con nuevas características para soportar esto. Como nadie ha tenido éxito, nadie está 100% seguro de qué características necesitarían tener. La mejor suposición actual es los tipos más altos (HKT).