RefCell<T>
y el Patrón de Mutabilidad Interior
La mutabilidad interna es un patrón de diseño en Rust que te permite mutar datos
incluso cuando hay referencias inmutables a esos datos; normalmente, esta acción
está prohibida por las reglas de borrowing. Para mutar datos, el patrón utiliza
código unsafe
dentro de una estructura de datos para flexibilizar las reglas
habituales de Rust que rigen la mutabilidad y el borrowing. El código unsafe
indica al compilador que estamos verificando las reglas manualmente en lugar de
confiar en que el compilador las verifique por nosotros; discutiremos el código
unsafe con más detalle en el Capítulo 19.
Podemos utilizar tipos que utilizan el patrón de mutabilidad interna solo cuando
podemos asegurar que las reglas de borrowing se seguirán en tiempo de ejecución,
aunque el compilador no pueda garantizarlo. El código unsafe
involucrado se
envuelve entonces en una API segura, y el tipo externo sigue siendo inmutable.
Vamos a explorar este concepto al examinar el tipo RefCell<T>
que sigue el
patrón de mutabilidad interna.
Cumpliendo las reglas de borrowing en tiempo de ejecución con RefCell<T>
A diferencia de Rc<T>
, el tipo RefCell<T>
representa un único ownership
sobre los datos que contiene. Entonces, ¿qué hace que RefCell<T>
sea diferente
de un tipo como Box<T>
? Recuerda las reglas de borrowing que aprendiste en el
Capítulo 4:
- En cualquier momento dado, puedes tener o bien una referencia mutable o bien cualquier número de referencias inmutables.
- Las referencias siempre deben ser válidas.
Con referencias y Box<T>
, las invariantes de las reglas de borrowing se hacen
cumplir en tiempo de compilación. Con RefCell<T>
, estas invariantes se hacen
cumplir en tiempo de ejecución. Con referencias, si rompes estas reglas,
obtendrás un error de compilación. Con RefCell<T>
, si rompes estas reglas, tu
programa entrará en panic y saldrá.
La ventaja de comprobar las reglas de borrowing en tiempo de compilación es que los errores se detectarán antes en el proceso de desarrollo, y no hay impacto en el rendimiento en tiempo de ejecución porque todo el análisis se completa de antemano. Por estas razones, comprobar las reglas de borrowing en tiempo de compilación es la mejor opción en la mayoría de los casos, por lo que esta es la opción predeterminada de Rust.
La ventaja de comprobar las reglas de borrowing en tiempo de ejecución es que se permiten ciertos escenarios seguros de memoria, donde habrían sido rechazados por las comprobaciones en tiempo de compilación. El análisis estático, como el compilador de Rust, es inherentemente conservador. Algunas propiedades del código son imposibles de detectar analizando el código: el ejemplo más famoso es el Problema de la Parada, que está fuera del alcance de este libro, pero es un tema interesante para investigar.
Debido a que algunos análisis son imposibles, si el compilador de Rust no puede
estar seguro de que el código cumple con las reglas de ownership, podría
rechazar un programa correcto; de esta manera, es conservador. Si Rust aceptara
un programa incorrecto, los usuarios no podrían confiar en las garantías que
Rust hace. Sin embargo, si Rust rechaza un programa correcto, el programador se
verá perjudicado, pero no puede ocurrir nada catastrófico. El tipo RefCell<T>
es útil cuando estás seguro de que tu código sigue las reglas de borrowing, pero
el compilador no puede entenderlo y garantizarlo.
Similar a Rc<T>
, RefCell<T>
solo se usa en escenarios de un solo hilo y te
dará un error de tiempo de compilación si intentas usarlo en un contexto
multihilo. Hablaremos de cómo obtener la funcionalidad de RefCell<T>
en un
programa multihilo en el Capítulo 16.
Aquí tienes un resumen de las razones para elegir Box<T>
, Rc<T>
o
RefCell<T>
:
Rc<T>
permite múltiples propietarios de los mismos datos;Box<T>
yRefCell<T>
tienen un único propietario.Box<T>
permite borrowing inmutable o mutable verificado en tiempo de compilación;Rc<T>
permite solo borrowing inmutable verificado en tiempo de compilación;RefCell<T>
permite borrowing inmutable o mutable verificado en tiempo de ejecución.- Debido a que
RefCell<T>
permite borrowing mutable verificado en tiempo de ejecución, puedes mutar el valor dentro de laRefCell<T>
incluso cuando laRefCell<T>
es inmutable.
Mutar el valor dentro de un valor inmutable es el patrón de mutabilidad interior. Veamos una situación en la que la mutabilidad interior es útil y examinemos cómo es posible.
Mutabilidad Interior: Un Borrow Mutable a un Valor Inmutable
Una consecuencia de las reglas de borrowing es que cuando tienes un valor inmutable, no puedes pedir prestado una referencia mutable a través de ese valor. Por ejemplo, este código no compilará:
fn main() {
let x = 5;
let y = &mut x;
}
Si intentas compilar este código, obtendrás el siguiente error:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
Sin embargo, hay situaciones en las que sería útil que un valor se mute a sí
mismo en sus métodos, pero parezca inmutable para otro código. El código fuera
de los métodos del valor no podría mutar el valor. Usar RefCell<T>
es una
forma de obtener la capacidad de tener mutabilidad interior, pero RefCell<T>
no evita las reglas de borrowing por completo: el comprobador de préstamos en el
compilador permite esta mutabilidad interior, y las reglas de borrowing se
comprueban en tiempo de ejecución en su lugar. Si violas las reglas, obtendrás
un panic!
en lugar de un error del compilador.
Vamos a trabajar a través de un ejemplo práctico donde podemos usar RefCell<T>
para mutar un valor inmutable y ver por qué es útil.
Un Caso de Uso para la Mutabilidad Interior: Mock Objects
A veces durante el testing, un programador usará un tipo en lugar de otro para observar un comportamiento particular y afirmar que se implementa correctamente. Este tipo de marcador de posición se llama test double. Piensa en ello en el sentido de un "doble de riesgo" en la realización de películas, donde una persona entra y sustituye a un actor para hacer una escena particularmente difícil. Los test doubles se sustituyen por otros tipos cuando se ejecutan las pruebas. Los objetos simulados son tipos específicos de test doubles que registran lo que sucede durante una prueba para que puedas afirmar que se produjeron las acciones correctas.
Rust no tiene objetos en el mismo sentido que otros lenguajes tienen objetos, y Rust no tiene funcionalidad de objetos simulados integrada en la biblioteca estándar como lo hacen otros lenguajes. Sin embargo, definitivamente puedes crear una struct que sirva para los mismos propósitos que un objeto simulado.
Aquí está el escenario que vamos a probar: crearemos una biblioteca que realiza un seguimiento de un valor en relación con un valor máximo, y envía mensajes en función de la proximidad del valor actual al valor máximo. Esta biblioteca podría usarse para realizar un seguimiento de la cuota de un usuario para el número de llamadas a la API que se le permite realizar, por ejemplo.
El objetivo de nuestra biblioteca es proporcionar la funcionalidad de realizar
un seguimiento de qué tan cerca está un valor de su máximo y que mensajes se
deben enviar en qué momentos. Se espera que las aplicaciones que utilicen
nuestra biblioteca proporcionen el mecanismo para enviar los mensajes: la
aplicación podría poner un mensaje en la interfaz de la aplicación, enviar un
correo electrónico, enviar un mensaje de texto o algo más. La biblioteca no
necesita saber ese detalle. Todo lo que necesita es algo que implemente un
trait que proporcionaremos llamado Messenger
. El listado 15-20 muestra el
código de la biblioteca:
Filename: src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Listing 15-20: Una biblioteca para realizar un seguimiento de qué tan cerca está un valor a su valor máximo y emitir advertencias cuando el valor alcanza ciertos niveles.
Una parte importante de este código es el trait Messenger
, que tiene un método
llamado send
que toma una referencia inmutable a self
y el texto del
mensaje. Este trait es la interfaz que nuestro objeto simulado necesita
implementar para que el simulado se pueda usar de la misma manera que un objeto
real. La otra parte importante es que queremos probar el comportamiento del
método set_value
en el LimitTracker
. Podemos cambiar lo que pasamos para el
parámetro value
, pero set_value
no devuelve nada para que podamos hacer
afirmaciones. Queremos poder decir que si creamos un LimitTracker
con algo
que implemente el trait Messenger
y un valor particular para max
, cuando
pasemos diferentes números para value
, se le dice al mensajero que envíe los
mensajes apropiados.
Necesitamos un objeto simulado que, en lugar de enviar un email o un mensaje de
texto cuando llamamos a send
, solo haga un seguimiento de los mensajes que se
le dice que envíe. Podemos crear una nueva instancia del objeto simulado,
crear un LimitTracker
que use el objeto simulado, llamar al método
set_value
en LimitTracker
y luego verificar que el objeto simulado tenga los
mensajes que esperamos. El listado 15-21 muestra un intento de implementar un
objeto simulado para hacer precisamente eso, pero el borrow checker
no lo permite:
Filename: src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
Listing 15-21: Un intento de implementar un MockMessenger
que no es permitido por el borrow checker
Este código de test define un struct MockMessenger
que tiene un campo
sent_messages
que es una Vec
de String
valores. Definimos una función
asociada new
para que sea conveniente crear nuevos valores MockMessenger
que comiencen con una lista vacía de mensajes. Luego implementamos el trait
Messenger
para MockMessenger
para que podamos darle un MockMessenger
a un
LimitTracker
. En la definición del método send
, tomamos el mensaje pasado
como parámetro y lo almacenamos en la lista MockMessenger
de sent_messages
.
En el test, estamos testeando qué sucede cuando el LimitTracker
se le dice que
establezca value
en algo que es más del 75 por ciento del valor max
. En
primer lugar, creamos un nuevo MockMessenger
, que comenzará con una lista
vacía de mensajes. Luego creamos un nuevo LimitTracker
y le damos una
referencia al nuevo MockMessenger
y un valor max
de 100. Llamamos al método
set_value
en el LimitTracker
con un valor de 80, que es más del 75 por
ciento de 100. Luego afirmamos que la lista de mensajes que el MockMessenger
está realizando un seguimiento debería tener ahora un mensaje en ella.
Sin embargo, hay un problema con este test, como se muestra aquí:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
2 | fn send(&mut self, msg: &str);
| ~~~~~~~~~
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
No podemos modificar sent_messages
para realizar un seguimiento de los
mensajes, porque el método send
toma una referencia inmutable a self
.
Tampoco podemos tomar la sugerencia del texto de error para usar &mut self
en su lugar, porque entonces la firma de send
no coincidiría con la firma en
la definición del trait Messenger
(siéntase libre de intentarlo y ver qué
mensaje de error obtiene).
Esta es una situación en la que la mutabilidad interior puede ayudar.
Almacenaremos los sent_messages
dentro de un RefCell<T>
, y luego el método
send
podrá modificar sent_messages
para almacenar los mensajes que hemos
visto. El listado 15-22 muestra cómo se ve eso:
Filename: src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Listing 15-22: Usando RefCell<T>
para mutar un valor
interno mientras el valor externo se considera inmutable.
El campo sent_messages
ahora es de tipo RefCell<Vec<String>>
en lugar de
Vec<String>
. En la función new
, creamos una nueva instancia de
RefCell<Vec<String>>
alrededor del vector vacío.
En la implementación del método send
, el primer parámetro sigue siendo un
inmutable borrow de self
, que coincide con la definición del trait. Llamamos
a borrow_mut
en el RefCell<Vec<String>>
en self.sent_messages
para obtener
una referencia mutable al valor dentro del RefCell<Vec<String>>
, que es el
vector. Luego podemos llamar a push
en la referencia mutable al vector para
hacer un seguimiento de los mensajes enviados durante el test.
La última modificación que debemos hacer está en la afirmación: para ver cuántos
elementos hay en el vector interno, llamamos a borrow
en el
RefCell<Vec<String>>
para obtener una referencia inmutable al vector.
Ahora que has visto cómo usar RefCell<T>
, ¡profundicemos en cómo funciona!
Haciendo un seguimiento del borrowing en runtime con RefCell<T>
Cuando creamos referencias inmutables y mutables, usamos la sintaxis &
y
&mut
, respectivamente. Con RefCell<T>
, usamos los métodos borrow
y
borrow_mut
, que son parte de la API segura que pertenece a RefCell<T>
. El
método borrow
devuelve el tipo de smart pointer Ref<T>
, y borrow_mut
devuelve el tipo de smart pointer RefMut<T>
. Ambos tipos implementan Deref
,
por lo que podemos tratarlos como referencias regulares.
RefCell<T>
realiza un seguimiento de cuántos smart pointers Ref<T>
y
RefMut<T>
están actualmente activos. Cada vez que llamamos a borrow
, el
RefCell<T>
aumenta su recuento de cuántos borrowing inmutables están activos.
Cuando un valor Ref<T>
sale del scope, el recuento de borrowing inmutables
disminuye en uno. Al igual que las reglas de borrowing en tiempo de compilación,
RefCell<T>
nos permite tener muchos borrowing inmutables o un borrowing
mutable en un momento dado.
Si intentamos romper estas reglas, en lugar de obtener un error del compilador
como lo haríamos con las referencias, la implementación de RefCell<T>
se
bloqueará en tiempo de ejecución. El listado 15-23 muestra una modificación de
la implementación de send
en el listado 15-22. Estamos tratando
deliberadamente de crear dos borrowing mutables activos para el mismo scope
para ilustrar que RefCell<T>
nos impide hacer esto en tiempo de ejecución.
Filename: src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Listing 15-23: Creando dos referencias mutables en el
mismo scope para ver que RefCell<T>
lanzará un panic
Creamos una variable one_borrow
para el smart pointer RefMut<T>
devuelto
desde borrow_mut
. Luego creamos otro borrowing mutable de la misma manera en
la variable two_borrow
. Esto hace dos referencias mutables en el mismo scope,
lo cual no está permitido. Cuando ejecutamos los tests para nuestra librería, el
código en el listado 15-23 se compilará sin errores, pero el test fallará:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Observa que el código entró en panic con el mensaje already borrowed: BorrowMutError
. Así es como RefCell<T>
maneja las violaciones de las reglas
de borrowing en tiempo de ejecución.
Elegir capturar errores de borrowing en tiempo de ejecución en lugar de en
tiempo de compilación, como lo hemos hecho aquí, significa que potencialmente
encontrarías errores en tu código más tarde en el proceso de desarrollo:
posiblemente no hasta que tu código se implemente en producción. Además, tu
código incurriría en una pequeña penalización de rendimiento en tiempo de
ejecución como resultado de realizar un seguimiento de los borrows en tiempo de
ejecución en lugar de en tiempo de compilación. Sin embargo, usar RefCell<T>
hace posible escribir un objeto simulado que pueda modificarse para realizar un
seguimiento de los mensajes que ha visto mientras lo estás usando en un
contexto donde solo se permiten valores inmutables. Puedes usar RefCell<T>
a pesar de sus compensaciones para obtener más funcionalidad de la que
proporcionan las referencias regulares.
Teniendo múltiples propietarios de datos mutables combinando Rc<T>
y RefCell<T>
Una forma común de usar RefCell<T>
es en combinación con Rc<T>
. Recuerda
que Rc<T>
te permite tener múltiples propietarios de algunos datos, pero solo
te da acceso inmutable a esos datos. Si tienes un Rc<T>
que contiene un
RefCell<T>
, puedes obtener un valor que puede tener múltiples propietarios y
que puedes mutar.
Por ejemplo, recuerda el ejemplo de la lista de cons en el Listado 15-18 donde
usamos Rc<T>
para permitir que múltiples listas compartan propiedad de otra
lista. Debido a que Rc<T>
contiene solo valores inmutables, no podemos cambiar
ninguno de los valores en la lista una vez que los hemos creado. Agreguemos
RefCell<T>
para obtener la capacidad de cambiar los valores en las listas.
El listado 15-24 muestra que al usar un RefCell<T>
en la definición de Cons
,
podemos modificar el valor almacenado en todas las listas:
Filename: src/main.rs
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }
Listing 15-24: Usando Rc<RefCell<i32>>
para crear una
List
que podemos modificar.
Creamos un valor que es una instancia de Rc<RefCell<i32>>
y lo almacenamos en
una variable llamada value
para que podamos acceder a él directamente más
tarde. Luego creamos una List
en a
con una variante Cons
que contiene
value
. Necesitamos clonar value
para que tanto a
como value
tengan
ownership del valor interno 5
en lugar de transferir el ownership de value
a a
o tener a
pedir prestado de value
.
Envolvemos la lista a
en un Rc<T>
para que cuando creemos las listas b
y
c
, ambas puedan referirse a a
, que es lo que hicimos en el listado 15-18.
Después de haber creado las listas en a
, b
y c
, queremos agregar 10 al
valor en value
. Hacemos esto llamando a borrow_mut
en value
, que usa la
característica de dereferenciación automática que discutimos en el capítulo 5
(ver la sección “¿Dónde está el operador ->
?”) para desreferenciar el Rc<T>
al valor interno RefCell<T>
.
El método borrow_mut
devuelve un smart pointer RefMut<T>
, y usamos el
operador de desreferenciación en él y cambiamos el valor interno.
Cuando imprimimos a
, b
y c
, podemos ver que todos tienen el valor
modificado de 15 en lugar de 5:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
¡Esta técnica es bastante genial! Al usar RefCell<T>
, tenemos un valor
List
externamente inmutable. Pero podemos usar los métodos en RefCell<T>
que proporcionan acceso a su mutabilidad interior para que podamos modificar
nuestros datos cuando sea necesario. Las comprobaciones en tiempo de ejecución
de las reglas de borrowing nos protegen de las condiciones de carrera en los
datos y, a veces, vale la pena intercambiar un poco de velocidad por esta
flexibilidad en nuestras estructuras de datos. ¡Ten en cuenta que RefCell<T>
no funciona para código multihilo! Mutex<T>
es la versión segura para hilos
de RefCell<T>
y discutiremos Mutex<T>
en el capítulo 16.