¿Abuso de mutabilidad interior en el diseño de API?
Mi experiencia en C ++ me incomoda acerca de la mutabilidad interior . El siguiente código es mi investigación sobre este tema.
Estoy de acuerdo en que, desde el punto de vista del verificador de préstamos, es imposible tratar con muchas referencias en cada estructura, cuyo estado interno podría modificarse tarde o temprano; ahí es claramente donde la mutabilidad interior puede ayudar.
Por otra parte, en el capítulo 15.5 "RefCell y el patrón Mutabilidad interior" de la roya Lenguaje de programación , el ejemplo de la Messenger
característica y su aplicación en la MockMessenger
estructura me hace pensar que se trata de un diseño de API común para preferir sistemáticamente &self
más &mut self
aún si es bastante obvio que algún tipo de mutabilidad será obligatorio tarde o temprano. ¿Cómo podría una implementación Messenger
no alterar su estado interno al enviar un mensaje? La excepción es simplemente imprimir el mensaje, lo cual es consistente con &self
, pero el caso general probablemente consistiría en escribir en algún tipo de flujo interno, lo que podría implicar almacenamiento en búfer, actualización de indicadores de error ... Todo esto ciertamente requiere &mut self
, como por ejemploimpl Write for File.
Depender de la mutabilidad interior para resolver este problema me suena como, en C ++, const_cast
abusar o abusar de los mutable
miembros solo porque en otras partes de la aplicación no fuimos consistentes sobre la const
ness (error común para los estudiantes de C ++).
Entonces, volviendo a mi código de ejemplo a continuación, debería:
- uso
&mut self
(el compilador no se queja, incluso si no es obligatorio) dechange_e()
achange_i()
para mantener la coherencia con el hecho de que modifico los valores de los enteros almacenados? - seguir usando
&self
, porque la mutabilidad interior lo permite, incluso si realmente modifico los valores de los enteros almacenados?
Esta decisión no solo es local a la estructura en sí, sino que tendrá una gran influencia en lo que podría expresarse en la aplicación utilizando esta estructura. La segunda solución ciertamente ayudará mucho, porque solo se incluyen referencias compartidas, pero es consistente con lo que se espera en Rust.
No puedo encontrar una respuesta a esta pregunta en las Pautas de la API de Rust . ¿Existe alguna otra documentación de Rust similar a C ++ CoreGuidelines ?
/*
$ rustc int_mut.rs && ./int_mut
initial: 1 2 3 4 5 6 7 8 9
change_a: 11 2 3 4 5 6 7 8 9
change_b: 11 22 3 4 5 6 7 8 9
change_c: 11 22 33 4 5 6 7 8 9
change_d: 11 22 33 44 5 6 7 8 9
change_e: 11 22 33 44 55 6 7 8 9
change_f: 11 22 33 44 55 66 7 8 9
change_g: 11 22 33 44 55 66 77 8 9
change_h: 11 22 33 44 55 66 77 88 9
change_i: 11 22 33 44 55 66 77 88 99
*/
struct Thing {
a: i32,
b: std::boxed::Box<i32>,
c: std::rc::Rc<i32>,
d: std::sync::Arc<i32>,
e: std::sync::Mutex<i32>,
f: std::sync::RwLock<i32>,
g: std::cell::UnsafeCell<i32>,
h: std::cell::Cell<i32>,
i: std::cell::RefCell<i32>,
}
impl Thing {
fn new() -> Self {
Self {
a: 1,
b: std::boxed::Box::new(2),
c: std::rc::Rc::new(3),
d: std::sync::Arc::new(4),
e: std::sync::Mutex::new(5),
f: std::sync::RwLock::new(6),
g: std::cell::UnsafeCell::new(7),
h: std::cell::Cell::new(8),
i: std::cell::RefCell::new(9),
}
}
fn show(&self) -> String // & is enough (read-only)
{
format!(
"{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
self.a,
self.b,
self.c,
self.d,
self.e.lock().unwrap(),
self.f.read().unwrap(),
unsafe { *self.g.get() },
self.h.get(),
self.i.borrow(),
)
}
fn change_a(&mut self) // &mut is mandatory
{
let target = &mut self.a;
*target += 10;
}
fn change_b(&mut self) // &mut is mandatory
{
let target = self.b.as_mut();
*target += 20;
}
fn change_c(&mut self) // &mut is mandatory
{
let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
*target += 30;
}
fn change_d(&mut self) // &mut is mandatory
{
let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
*target += 40;
}
fn change_e(&self) // !!! no &mut here !!!
{
// With C++, a std::mutex protecting a separate integer (e)
// would have been used as two data members of the structure.
// As our intent is to alter the integer (e), and because
// std::mutex::lock() is _NOT_ const (but it's an internal
// that could have been hidden behind the mutable keyword),
// this member function would _NOT_ be const in C++.
// But here, &self (equivalent of a const member function)
// is accepted although we actually change the internal
// state of the structure (the protected integer).
let mut target = self.e.lock().unwrap();
*target += 50;
}
fn change_f(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e)
let mut target = self.f.write().unwrap();
*target += 60;
}
fn change_g(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e, f)
let target = self.g.get();
unsafe { *target += 70 };
}
fn change_h(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e, f, g)
self.h.set(self.h.get() + 80);
}
fn change_i(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e, f, g, h)
let mut target = self.i.borrow_mut();
*target += 90;
}
}
fn main() {
let mut t = Thing::new();
println!(" initial: {}", t.show());
t.change_a();
println!("change_a: {}", t.show());
t.change_b();
println!("change_b: {}", t.show());
t.change_c();
println!("change_c: {}", t.show());
t.change_d();
println!("change_d: {}", t.show());
t.change_e();
println!("change_e: {}", t.show());
t.change_f();
println!("change_f: {}", t.show());
t.change_g();
println!("change_g: {}", t.show());
t.change_h();
println!("change_h: {}", t.show());
t.change_i();
println!("change_i: {}", t.show());
}
Respuestas
Depender de la mutabilidad interior para resolver este problema me suena como, en C ++,
const_cast
abusar o abusar de losmutable
miembros solo porque en otras partes de la aplicación no fuimos consistentes sobre laconst
ness (error común para los estudiantes de C ++).
Este es un pensamiento completamente comprensible en el contexto de C ++. La razón por la que no es precisa es porque C ++ y Rust tienen diferentes conceptos de mutabilidad.
En cierto modo, la mut
palabra clave de Rust tiene dos significados. En un patrón significa "mutable" y en un tipo de referencia significa "exclusivo". La diferencia entre &self
y &mut self
no es realmente si self
se puede mutar o no, sino si se puede usar un alias .
En el Messenger
ejemplo, bueno, primero no nos lo tomemos demasiado en serio; está destinado a ilustrar las características del lenguaje, no necesariamente al diseño del sistema. Pero podemos imaginar por qué &self
podría usarse: Messenger
está destinado a ser implementado por estructuras que son compartidas , por lo que diferentes piezas de código pueden contener referencias al mismo objeto y usarlo para send
alertas sin coordinarse entre sí. Si send
se tomara &mut self
, sería inútil para este propósito porque solo puede existir una &mut self
referencia a la vez. Sería imposible enviar mensajes a un compartido Messenger
(sin agregar una capa externa de mutabilidad interior a través de Mutex
o algo así).
Por otro lado, cada referencia y puntero de C ++ puede tener un alias.¹ Entonces, en términos de Rust, ¡ toda mutabilidad en C ++ es mutabilidad "interior"! Rust no tiene equivalente mutable
en C ++ porque Rust no tiene const
miembros (el eslogan aquí es "la mutabilidad es una propiedad de la unión, no del tipo"). Rust hace tener un equivalente a const_cast
, pero sólo para los punteros primas, porque es poco sólido para convertir una compartida &
referencia en un exclusivo &mut
referencia. Por el contrario, C ++ no tiene nada como Cell
o RefCell
porque cada valor está implícitamente detrás de un UnsafeCell
ya.
Entonces, volviendo a mi código de ejemplo a continuación, ¿debería [...]
Realmente depende de la semántica prevista de Thing
. ¿Es la naturaleza de Thing
ser compartido, como un punto final de canal o un archivo? ¿Tiene sentido change_e
que se le llame en una referencia compartida (con alias)? Si es así, utilice la mutabilidad interior para exponer un método &self
. ¿Es Thing
principalmente un contenedor de datos? ¿A veces tiene sentido que sea compartido y, a veces, exclusivo? Entonces Thing
probablemente no debería usar la mutabilidad interior y dejar que el usuario de la biblioteca decida cómo lidiar con la mutación compartida, si fuera necesario.
Ver también
- ¿Cuál es la diferencia entre colocar "mut" antes de un nombre de variable y después de ":"?
- ¿Por qué la mutabilidad de una variable no se refleja en su tipo de firma en Rust?
- Necesita una explicación holística sobre los tipos contados de referencia y celda de Rust
¹ En realidad, C ++ hace tener una característica que hace que los punteros funcionan de manera similar a las referencias en Rust. Mas o menos. restrict
es una extensión no estándar en C ++ pero es parte de C99. Las &
referencias compartidas ( ) de Rust son como const *restrict
punteros, y las &mut
referencias exclusivas ( ) son como no const
*restrict
punteros. Consulte ¿Qué significa la palabra clave restringir en C ++?
¿Cuándo fue la última vez que usó deliberadamente un puntero restrict
(o __restrict
, etc.) en C ++? No se moleste en pensar en ello; la respuesta es "nunca". restrict
permite optimizaciones más agresivas que los punteros normales, pero es muy difícil usarlo correctamente porque debe tener mucho cuidado con el aliasing y el compilador no ofrece ayuda. Básicamente es una pistola enorme y casi nadie la usa. Para que valga la pena usar de manera restrict
generalizada la forma en que usa const
en C ++, necesitaría poder anotar en las funciones qué punteros pueden usar alias de otros en qué momentos, establecer algunas reglas sobre cuándo es válido seguir los punteros, y tener un pase de compilador que compruebe si se siguen las reglas en cada función. Como una especie de ... corrector.