rust - Situaciones donde Cell o RefCell es la mejor opción
interior-mutability (3)
¿Cuándo se le requerirá usar
Cell o RefCell
?
Parece que hay muchas otras opciones de tipo que serían adecuadas en lugar de estas, y la documentación advierte que usar
RefCell
es un poco como un "último recurso".
¿Usar estos tipos es un "
olor a código
"?
¿Alguien puede mostrar un ejemplo en el que usar estos tipos tenga más sentido que usar otro tipo, como
Rc
o incluso
Box
?
No es del todo correcto preguntar cuándo se debe usar
Cell
o
RefCell
sobre
Box
y
Rc
porque estos tipos resuelven diferentes problemas.
De hecho, la mayoría de las veces
RefCell
se usa
junto
con
Rc
para proporcionar mutabilidad con propiedad compartida.
Entonces, sí, los casos de uso para
Cell
y
RefCell
dependen completamente de los requisitos de mutabilidad en su código.
La mutabilidad interior y exterior se explica muy bien en el libro oficial de Rust, en el capítulo designado sobre mutabilidad . La mutabilidad externa está muy ligada al modelo de propiedad, y sobre todo cuando decimos que algo es mutable o inmutable, nos referimos exactamente a la mutabilidad externa. Otro nombre para la mutabilidad externa es la mutabilidad heredada , lo que probablemente explica el concepto más claramente: este tipo de mutabilidad está definido por el propietario de los datos y se hereda a todo lo que puede obtener del propietario. Por ejemplo, si su variable de tipo estructural es mutable, también lo son todos los campos de la estructura en la variable:
struct Point { x: u32, y: u32 }
// the variable is mutable...
let mut p = Point { x: 10, y: 20 };
// ...and so are fields reachable through this variable
p.x = 11;
p.y = 22;
let q = Point { x: 10, y: 20 };
q.x = 33; // compilation error
La mutabilidad heredada también define qué tipos de referencias puede obtener del valor:
{
let px: &u32 = &p.x; // okay
}
{
let py: &mut u32 = &mut p.x; // okay, because p is mut
}
{
let qx: &u32 = &q.x; // okay
}
{
let qy: &mut u32 = &mut q.y; // compilation error since q is not mut
}
A veces, sin embargo, la mutabilidad hereditaria no es suficiente.
El ejemplo canónico es el puntero contado por referencia, llamado
Rc
en Rust.
El siguiente código es completamente válido:
{
let x1: Rc<u32> = Rc::new(1);
let x2: Rc<u32> = x1.clone(); // create another reference to the same data
let x3: Rc<u32> = x2.clone(); // even another
} // here all references are destroyed and the memory they were pointing at is deallocated
A primera vista, no está claro cómo se relaciona la mutabilidad con esto, pero recuerde que los punteros contados por referencia se llaman así porque contienen un contador de referencia interno que se modifica cuando una referencia se duplica (
clone()
en Rust) y se destruye ( se sale del alcance en
Rust
).
Por lo tanto,
Rc
tiene
que modificarse aunque esté almacenado dentro de una variable que no sea
mut
.
Esto se logra a través de la mutabilidad interna.
Hay tipos especiales en la biblioteca estándar, el más básico de ellos es
UnsafeCell
, que permite trabajar alrededor de las reglas de mutabilidad externa y mutar algo incluso si está almacenado (transitivamente) en una variable que no sea
mut
.
Otra forma de decir que algo tiene mutabilidad interna es que este algo puede modificarse a través de una referencia
&
, es decir, si tiene un valor de tipo
&T
y puede modificar el estado de
T
que apunta, entonces
T
tiene mutabilidad.
Por ejemplo, la
Cell
puede contener datos de
Copy
y puede mutarse incluso si se almacena en una ubicación que no sea
mut
:
let c: Cell<u32> = Cell::new(1);
c.set(2);
assert_eq!(c.get(), 2);
RefCell
puede contener datos que no son de
Copy
y puede proporcionarle punteros
&mut
a su valor contenido, y la ausencia de alias se verifica en tiempo de ejecución.
Todo esto se explica en detalle en sus páginas de documentación.
Al final resultó que, en un abrumador número de situaciones, puede fácilmente ir solo con mutabilidad externa.
La mayor parte del código de alto nivel existente en Rust está escrito de esa manera.
A veces, sin embargo, la mutabilidad interna es inevitable o hace que el código sea mucho más claro.
Un ejemplo, la implementación de
Rc
, ya se describió anteriormente.
Otra es cuando necesita una propiedad mutable compartida (es decir, necesita acceder y modificar el mismo valor desde diferentes partes de su código); esto generalmente se logra a través de
Rc<RefCell<T>>
, porque no se puede hacer con referencias solo.
Incluso otro ejemplo es
Arc<Mutex<T>>
,
Mutex
es otro tipo de mutabilidad interna que también es seguro de usar en subprocesos.
Entonces, como puede ver,
Cell
y
RefCell
no son reemplazos de
Rc
o
Box
;
resuelven la tarea de proporcionarle mutabilidad en algún lugar donde no esté permitido por defecto.
Puede escribir su código sin usarlos en absoluto;
y si te encuentras en una situación en la que los necesitarías, lo sabrás.
Cell
y las
RefCell
no son código olfativo;
la única razón por la que se los describe como "último recurso" es que trasladan la tarea de verificar la mutabilidad y las reglas de alias del compilador al código de tiempo de ejecución, como en el caso de
RefCell
: no puede tener dos
&mut
apuntando al mismo datos al mismo tiempo, el compilador lo aplica de forma estática, pero con
RefCell
s puede pedirle al mismo
RefCell
que le brinde todos los
RefCell
que desee, excepto que si lo hace más de una vez, entrará en pánico, aplicar reglas de alias en tiempo de ejecución.
Podría decirse que los pánicos son peores que los errores de compilación porque solo puede encontrar los errores que los causan en tiempo de ejecución en lugar de en tiempo de compilación.
A veces, sin embargo, el analizador estático en el compilador es demasiado restrictivo, y de hecho necesita "solucionarlo".
No,
Cell
y
RefCell
no son "olores de código".
Normalmente, la mutabilidad se
hereda
, es decir, puede mutar un campo o una parte de una estructura de datos si y solo si tiene acceso exclusivo a toda la estructura de datos, y por lo tanto puede optar por la mutabilidad en ese nivel con
mut
(es decir,
foo.x
hereda
su mutabilidad o falta de ella de
foo
).
Este es un patrón muy poderoso y debe usarse siempre que funcione bien (lo cual es sorprendentemente frecuente).
Pero no es lo suficientemente expresivo para todo el código en todas partes.
Box
y
Rc
no tienen nada que ver con esto.
Como casi todos los otros tipos, respetan la mutabilidad heredada: puede mutar el contenido de un
Box
si tiene acceso exclusivo y mutable al
Box
(porque eso significa que también tiene acceso exclusivo a los contenidos).
Por el contrario, nunca puede obtener un
&mut
del contenido de un
Rc
porque, por su naturaleza,
Rc
es compartido (es decir, puede haber múltiples
Rc
s que se refieren a los mismos datos).
Un caso común de
Cell
o
RefCell
es que necesita compartir datos mutables entre varios lugares.
Tener dos referencias
&mut
a los mismos datos normalmente no está permitido (¡y por una buena razón!).
Sin embargo, a veces lo
necesita
, y los tipos de células permiten hacerlo de forma segura.
Esto podría hacerse a través de la combinación común de
Rc<RefCell<T>>
, que permite que los datos permanezcan durante el tiempo que cualquiera lo use y permite que todos (¡pero solo uno a la vez!) Lo muten.
O podría ser tan simple como
&Cell<i32>
(incluso si la celda está envuelta en un tipo más significativo).
Este último también se usa comúnmente para
estados internos, privados y mutables
como recuentos de referencia.
La documentación en realidad tiene varios ejemplos de dónde usaría
Cell
o
RefCell
.
Un buen ejemplo es en realidad
Rc
mismo.
Al crear un nuevo
Rc
, el recuento de referencia debe aumentarse, pero el recuento de referencia se comparte entre todos los
Rc
, por lo que, por mutabilidad heredada, esto no podría funcionar.
Rc
prácticamente
tiene
que usar una
Cell
.
Una buena guía es intentar escribir la mayor cantidad de código posible sin tipos de celda, pero usarlos cuando duele demasiado sin ellos. En algunos casos, hay una buena solución sin celdas y, con experiencia, podrá encontrarlas cuando las perdió anteriormente, pero siempre habrá cosas que simplemente no serían posibles sin ellas.
Suponga que desea o necesita crear algún objeto del tipo de su elección y volcarlo en un
Rc
.
let x = Rc::new(5i32);
Ahora, puede crear fácilmente otro
Rc
que apunte exactamente al mismo objeto y, por lo tanto, a la ubicación de la memoria:
let y = x.clone();
let yval: i32 = *y;
Dado que en Rust puede que nunca tenga una referencia mutable a una ubicación de memoria a la que exista alguna otra referencia, estos contenedores
Rc
nunca podrán modificarse nuevamente.
Entonces, ¿qué pasaría si quisieras poder modificar esos objetos
y
tener múltiples
Rc
apuntando a un mismo objeto?
Este es el problema que
Cell
y
RefCell
resuelven.
La solución se llama "mutabilidad interior", y significa que las reglas de alias de Rust se aplican en tiempo de ejecución en lugar de en tiempo de compilación.
De vuelta a nuestro ejemplo original:
let x = Rc::new(RefCell::new(5i32));
let y = x.clone();
Para obtener una referencia mutable a su tipo, use
borrow_mut
en
RefCell
.
let yval = x.borrow_mut();
*yval = 45;
En caso de que ya haya tomado prestado el valor al que apunta su
Rc
ya sea de forma mutable o no, la función
borrow_mut
entrará en pánico y, por lo tanto,
borrow_mut
las reglas de alias de Rust.
Rc<RefCell<T>>
es solo un ejemplo de
RefCell
, hay muchos otros usos legítimos.
Pero la documentación es correcta.
Si hay otra forma,
RefCell
, porque el compilador no puede ayudarlo a razonar sobre
RefCell
s.