RefCell<T> y el Patrón de Mutabilidad Interior

RustRustBeginner
Practicar Ahora

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í

Introducción

Bienvenido a RefCell y el Patrón de Mutabilidad Interior. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, exploraremos el concepto de mutabilidad interior en Rust y cómo se implementa utilizando el tipo RefCell<T>.

RefCell<T>{=html} y el Patrón de Mutabilidad Interior

La 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.

Aplicando las Reglas de Préstamo en Tiempo de Ejecución con RefCell<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:

  • En cualquier momento dado, puedes tener ya sea una referencia mutable o cualquier número de referencias inmutables (pero no ambas).
  • Las referencias deben siempre ser válidas.

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.
  • Debido a que 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.

Mutabilidad Interior: Un Préstamo Mutable a un Valor Inmutable

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.

Un Caso de Uso para la Mutabilidad Interior: Objetos de Simulación

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!

Mantenimiento del Control de Préstamos en Tiempo de Ejecución con RefCell<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.

Permitiendo Varios Propietarios de Datos Mutables con Rc<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.

Resumen

¡Felicitaciones! Has completado el laboratorio de RefCell y el Patrón de Mutabilidad Interior. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.