rust interior-mutability

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.