Introducción
Bienvenido a RefCell
En esta práctica, exploraremos el concepto de mutabilidad interior en Rust y cómo se implementa utilizando el tipo RefCell<T>
.
This tutorial is from open-source community. Access the source code
💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí
Bienvenido a RefCell
En esta práctica, exploraremos el concepto de mutabilidad interior en Rust y cómo se implementa utilizando el tipo RefCell<T>
.
<T>
{=html} y el Patrón de Mutabilidad InteriorLa mutabilidad interior 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 préstamo. Para mutar datos, el patrón utiliza código unsafe
dentro de una estructura de datos para desviar las reglas habituales de Rust que gobiernan la mutación y el préstamo. El código unsafe
indica al compilador que estamos comprobando las reglas manualmente en lugar de confiar en que el compilador las compruebe por nosotros; discutiremos el código unsafe
más en el Capítulo 19.
Sólo podemos utilizar tipos que usan el patrón de mutabilidad interior cuando podemos garantizar que se seguirán las reglas de préstamo en tiempo de ejecución, aunque el compilador no puede garantizarlo. El código unsafe
implicado se envuelve entonces en una API segura, y el tipo externo sigue siendo inmutable.
Exploremos este concepto mirando el tipo RefCell<T>
que sigue el patrón de mutabilidad interior.
<T>
{=html}A diferencia de Rc<T>
, el tipo RefCell<T>
representa la propiedad exclusiva de los datos que contiene. Entonces, ¿en qué se diferencia RefCell<T>
de un tipo como Box<T>
? Recuerda las reglas de préstamo que aprendiste en el Capítulo 4:
Con las referencias y Box<T>
, las invariantes de las reglas de préstamo se aplican en tiempo de compilación. Con RefCell<T>
, estas invariantes se aplican en tiempo de ejecución. Con las referencias, si violas estas reglas, obtendrás un error del compilador. Con RefCell<T>
, si violas estas reglas, tu programa se bloqueará y saldrá.
Las ventajas de comprobar las reglas de préstamo en tiempo de compilación son que los errores se detectarán más temprano en el proceso de desarrollo y no hay impacto en el rendimiento en tiempo de ejecución porque todo el análisis se completa previamente. Por esas razones, comprobar las reglas de préstamo en tiempo de compilación es la mejor opción en la mayoría de los casos, que es por qué es el predeterminado de Rust.
La ventaja de comprobar las reglas de préstamo en tiempo de ejecución en lugar de eso es que entonces se permiten ciertos escenarios seguros de memoria, donde hubieran sido prohibidos 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 al analizar 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 propiedad, puede 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 ofrece. Sin embargo, si Rust rechaza un programa correcto, el programador se verá inconvenciado, 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 préstamo pero el compilador no es capaz de entender y garantizarlo.
Similar a Rc<T>
, RefCell<T>
solo se debe utilizar en escenarios de un solo subproceso y te dará un error en tiempo de compilación si intentas usarlo en un contexto de varios subprocesos. Hablaremos sobre cómo obtener la funcionalidad de RefCell<T>
en un programa de varios subprocesos en el Capítulo 16.
A continuación, se resume las razones para elegir Box<T>
, Rc<T>
o RefCell<T>
:
Rc<T>
permite múltiples propietarios de los mismos datos; Box<T>
y RefCell<T>
tienen un solo propietario.Box<T>
permite préstamos inmutables o mutables comprobados en tiempo de compilación; Rc<T>
solo permite préstamos inmutables comprobados en tiempo de compilación; RefCell<T>
permite préstamos inmutables o mutables comprobados en tiempo de ejecución.RefCell<T>
permite préstamos mutables comprobados en tiempo de ejecución, puedes mutar el valor dentro de RefCell<T>
incluso cuando RefCell<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.
Una consecuencia de las reglas de préstamo es que cuando tienes un valor inmutable, no puedes préstamo mutarlo. Por ejemplo, este código no se compilará:
Nombre de archivo: src/main.rs
fn main() {
let x = 5;
let y = &mut x;
}
Si intentaras compilar este código, obtendrías el siguiente error:
error[E0596]: no se puede prestar `x` como mutable, ya que no está
declarado como mutable
--> src/main.rs:3:13
|
2 | let x = 5;
| - ayuda: considere cambiar esto a ser mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ no se puede prestar como mutable
Sin embargo, hay situaciones en las que sería útil que un valor se mutara a sí mismo en sus métodos pero apareciera inmutable para el resto del código. El código fuera de los métodos del valor no sería capaz de mutar el valor. Usar RefCell<T>
es una manera de obtener la capacidad de tener mutabilidad interior, pero RefCell<T>
no evita completamente las reglas de préstamo: el verificador de préstamos del compilador permite esta mutabilidad interior, y las reglas de préstamo se comprueban en tiempo de ejecución en lugar de eso. Si violas las reglas, obtendrás un panic!
en lugar de un error del compilador.
Veamos un ejemplo práctico en el que podemos usar RefCell<T>
para mutar un valor inmutable y veamos por qué eso es útil.
A veces, durante las pruebas, un programador usará un tipo en lugar de otro tipo, con el fin de observar un comportamiento particular y afirmar que está implementado correctamente. Este tipo de marcador de posición se llama doble de prueba. Piénsalo en el sentido de un doble de actuación en el cine, donde una persona entra y sustituye a un actor para hacer una escena particularmente complicada. Los dobles de prueba sustituyen a otros tipos cuando estamos ejecutando pruebas. Los objetos de simulación son tipos específicos de dobles de prueba que registran lo que sucede durante una prueba para que puedas afirmar que se llevaron a cabo las acciones correctas.
Rust no tiene objetos en el mismo sentido que otros lenguajes y no tiene la funcionalidad de objetos de simulación integrada en la biblioteca estándar como lo hacen algunos otros lenguajes. Sin embargo, definitivamente puedes crear una estructura que servirá para los mismos fines que un objeto de simulación.
Aquí está el escenario que probaremos: crearemos una biblioteca que sigue un valor en relación con un valor máximo y envía mensajes según qué tan cerca del valor máximo está el valor actual. Esta biblioteca podría usarse, por ejemplo, para controlar la cuota de un usuario para el número de llamadas API que se le permite hacer.
Nuestra biblioteca solo proporcionará la funcionalidad de controlar qué tan cerca del máximo está un valor y cuáles deben ser los mensajes en cada momento. Las aplicaciones que usen nuestra biblioteca se esperará que proporcionen el mecanismo para enviar los mensajes: la aplicación podría poner un mensaje en la aplicación, enviar un correo electrónico, enviar un mensaje de texto o hacer algo más. La biblioteca no necesita saber ese detalle. Todo lo que necesita es algo que implemente un trato que proporcionaremos llamado Messenger
. La Lista 15-20 muestra el código de la biblioteca.
Nombre de archivo: src/lib.rs
pub trait Messenger {
1 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,
}
}
2 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: You're at 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You're at 75% of your quota!");
}
}
}
Lista 15-20: Una biblioteca para controlar qué tan cerca está un valor de un valor máximo y advertir cuando el valor está en ciertos niveles
Una parte importante de este código es que el trato Messenger
tiene un método llamado send
que toma una referencia inmutable a self
y el texto del mensaje [1]. Este trato es la interfaz que nuestro objeto de simulación necesita implementar para que se pueda usar el objeto de simulación 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
[2]. Podemos cambiar lo que pasamos para el parámetro value
, pero set_value
no devuelve nada para lo que podamos hacer afirmaciones. Queremos poder decir que si creamos un LimitTracker
con algo que implemente el trato Messenger
y un valor particular para max
, cuando pasamos diferentes números para value
se le dice al mensajero que envíe los mensajes adecuados.
Necesitamos un objeto de simulación que, en lugar de enviar un correo electrónico o un mensaje de texto cuando llamamos a send
, solo registrará los mensajes que se le dicen que envíe. Podemos crear una nueva instancia del objeto de simulación, crear un LimitTracker
que use el objeto de simulación, llamar al método set_value
en LimitTracker
y luego comprobar que el objeto de simulación tenga los mensajes que esperamos. La Lista 15-21 muestra un intento de implementar un objeto de simulación para hacer exactamente eso, pero el verificador de préstamos no lo permitirá.
Nombre de archivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
1 struct MockMessenger {
2 sent_messages: Vec<String>,
}
impl MockMessenger {
3 fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
4 impl Messenger for MockMessenger {
fn send(&self, message: &str) {
5 self.sent_messages.push(String::from(message));
}
}
#[test]
6 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);
}
}
Lista 15-21: Un intento de implementar un MockMessenger
que no está permitido por el verificador de préstamos
Este código de prueba define una estructura MockMessenger
[1] que tiene un campo sent_messages
con un Vec
de valores String
[2] para registrar los mensajes que se le dicen que envíe. También definimos una función asociada new
[3] para facilitar la creación de nuevos valores de MockMessenger
que empiecen con una lista vacía de mensajes. Luego implementamos el trato Messenger
para MockMessenger
[4] para que podamos dar un MockMessenger
a un LimitTracker
. En la definición del método send
[5], tomamos el mensaje pasado como parámetro y lo almacenamos en la lista sent_messages
de MockMessenger
.
En la prueba, estamos probando lo que sucede cuando se le dice a LimitTracker
que establezca value
en algo que es más del 75 por ciento del valor max
[6]. Primero 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 está registrando MockMessenger
ahora debería tener un mensaje en ella.
Sin embargo, hay un problema con esta prueba, como se muestra aquí:
error[E0596]: no se puede prestar `self.sent_messages` como mutable, ya que está detrás de una
`&` referencia
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- ayuda: considere cambiar eso a ser una referencia mutable:
`&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` es una
`&` referencia, por lo que los datos a los que se refiere no se pueden prestar como mutable
No podemos modificar el MockMessenger
para registrar los mensajes porque el método send
toma una referencia inmutable a self
. Tampoco podemos tomar la sugerencia del texto del 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 trato Messenger
(siéntase libre de probarlo 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
será capaz de modificar sent_messages
para almacenar los mensajes que hemos visto. La Lista 15-22 muestra cómo se ve eso.
Nombre de archivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
1 sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
2 sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages
3.borrow_mut()
.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
--snip--
assert_eq!(
4 mock_messenger.sent_messages.borrow().len(),
1
);
}
}
Lista 15-22: Usando RefCell<T>
para mutar un valor interno mientras el valor externo se considera inmutable
El campo sent_messages
ahora es del tipo RefCell<Vec<String>>
[1] en lugar de Vec<String>
. En la función new
, creamos una nueva instancia de RefCell<Vec<String>>
alrededor del vector vacío [2].
Para la implementación del método send
, el primer parámetro sigue siendo una préstamo inmutable de self
, lo que coincide con la definición del trato. Llamamos a borrow_mut
en el RefCell<Vec<String>>
en self.sent_messages
[3] 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 registrar los mensajes enviados durante la prueba.
El último cambio que tenemos que hacer es 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 [4].
Ahora que has visto cómo usar RefCell<T>
, profundicemos en cómo funciona!
<T>
{=html}Al crear 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 apuntador inteligente Ref<T>
, y borrow_mut
devuelve el tipo de apuntador inteligente RefMut<T>
. Ambos tipos implementan Deref
, por lo que podemos tratarlos como referencias normales.
RefCell<T>
mantiene un control de cuántos apuntadores inteligentes Ref<T>
y RefMut<T>
están actualmente activos. Cada vez que llamamos a borrow
, RefCell<T>
incrementa su cuenta de cuántos préstamos inmutables están activos. Cuando un valor Ref<T>
sale del ámbito, la cuenta de préstamos inmutables disminuye en 1. Al igual que las reglas de préstamo en tiempo de compilación, RefCell<T>
nos permite tener muchos préstamos inmutables o un préstamo mutable en cualquier momento.
Si intentamos violar 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. La Lista 15-23 muestra una modificación de la implementación de send
en la Lista 15-22. Estamos deliberadamente intentando crear dos préstamos mutables activos para el mismo ámbito para ilustrar que RefCell<T>
nos impide hacer esto en tiempo de ejecución.
Nombre de archivo: src/lib.rs
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));
}
}
Lista 15-23: Creando dos referencias mutables en el mismo ámbito para ver que RefCell<T>
se bloqueará
Creamos una variable one_borrow
para el apuntador inteligente RefMut<T>
devuelto por borrow_mut
. Luego creamos otro préstamo mutable de la misma manera en la variable two_borrow
. Esto crea dos referencias mutables en el mismo ámbito, lo cual no está permitido. Cuando ejecutamos las pruebas de nuestra biblioteca, el código en la Lista 15-23 se compilará sin errores, pero la prueba fallará:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Observe que el código se bloqueó con el mensaje already borrowed: BorrowMutError
. Así es como RefCell<T>
maneja las violaciones de las reglas de préstamo en tiempo de ejecución.
Elegir capturar errores de préstamo en tiempo de ejecución en lugar de en tiempo de compilación, como lo hemos hecho aquí, significa que posiblemente estarías encontrando errores en tu código más adelante en el proceso de desarrollo: posiblemente no hasta que tu código se haya desplegado en producción. Además, tu código sufrirá una pequeña penalización de rendimiento en tiempo de ejecución como resultado de mantener el control de los préstamos en tiempo de ejecución en lugar de en tiempo de compilación. Sin embargo, usar RefCell<T>
hace posible escribir un objeto de simulación que puede modificarse a sí mismo para registrar 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 que las referencias normales proporcionan.
<T>
{=html} y RefCell<T>
{=html}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 en la Lista 15-18 donde usamos Rc<T>
para permitir que múltiples listas compartan la propiedad de otra lista. Debido a que Rc<T>
solo contiene valores inmutables, una vez que los hemos creado no podemos cambiar ninguno de los valores de la lista. Vamos a agregar RefCell<T>
por su capacidad para cambiar los valores en las listas. La Lista 15-24 muestra que al usar un RefCell<T>
en la definición de Cons
, podemos modificar el valor almacenado en todas las listas.
Nombre de archivo: 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() {
1 let value = Rc::new(RefCell::new(5));
2 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));
3 *value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
Lista 15-24: Usando Rc<RefCell<i32>>
para crear una List
que podemos mutar
Creamos un valor que es una instancia de Rc<RefCell<i32>>
y lo almacenamos en una variable llamada value
[1] para poder acceder a él directamente más adelante. Luego creamos una List
en a
con una variante Cons
que contiene value
[2]. Necesitamos clonar value
para que tanto a
como value
tengan la propiedad del valor interno 5
en lugar de transferir la propiedad de value
a a
o que a
preste de value
.
Envuelve la lista a
en un Rc<T>
para que cuando creemos las listas b
y c
, ambas puedan hacer referencia a a
, como hicimos en la Lista 15-18.
Después de haber creado las listas en a
, b
y c
, queremos sumar 10 al valor en value
[3]. Hacemos esto llamando a borrow_mut
en value
, que utiliza la característica de desreferenciación automática que discutimos en "¿Dónde está el operador ->?" para desreferenciar el Rc<T>
al valor interno RefCell<T>
. El método borrow_mut
devuelve un apuntador inteligente 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
:
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 ingeniosa. Al usar RefCell<T>
, tenemos un valor List
inmutable en apariencia. 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 préstamo nos protegen de las carreras de datos, y a veces vale la pena intercambiar un poco de velocidad por esta flexibilidad en nuestras estructuras de datos. Tenga en cuenta que RefCell<T>
no funciona para el código de varios subprocesos. Mutex<T>
es la versión segura para subprocesos de RefCell<T>
, y discutiremos Mutex<T>
en el Capítulo 16.
¡Felicitaciones! Has completado el laboratorio de RefCell