Mutabilidad interior

Cell

La mutabilidad interior permite modificar el interior de un elemento sin necesidad de que la variable sea mut.

Rust permite hacer esto en algunos casos de forma segura, modificando los valores internos de un struct que es inmutable. Cada uno de los mecanismos que se tienen a disposición, sigue unas reglas que aseguran que la modificación es segura.

En primer lugar, se presenta el siguiente ejemplo:

struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: bool,
}

fn main() {
    let super_phone_3000 = PhoneModel {
        company_name: "YY Electronics".to_string(),
        model_name: "Super Phone 3000".to_string(),
        screen_size: 7.5,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: true,
    };

}

En este ejemplo, es mejor que los campos de PhoneModel sean inmutables. Por ejemplo, date_issued y screen_size nunca cambian.

Pero en su interior, también existe un campo denominado on_sale. Un modelo de teléfono empieza su vida estando a la venta (true en este campo), pero más tarde se deja de vender. ¿Se puede hacer que solo este campo sea modificable sin que se tenga que hacer todo el struct modificable? Es decir, sin tener que hacer let mut super_phone_3000 = ..., ya que de hacerlo así, todos los campos serían modificables.

Rust tiene varias formas de permitir la modificación segura dentro de un lugar que es inmutable en general. La forma más simple es el uso de Cell. Se tiene que incluir use std::cell::Cell para poder usarlo y a partir de ahí usar normalmente Cell.

use std::cell::Cell;

struct PhoneModel {
    company_name: String,
    model_name: String,
    screen_size: f32,
    memory: usize,
    date_issued: u32,
    on_sale: Cell<bool>,
}

fn main() {
    let super_phone_3000 = PhoneModel {
        company_name: "YY Electronics".to_string(),
        model_name: "Super Phone 3000".to_string(),
        screen_size: 7.5,
        memory: 4_000_000,
        date_issued: 2020,
        on_sale: Cell::new(true),
    };

    // 10 years later, super_phone_3000 is not on sale anymore
    super_phone_3000.on_sale.set(false);
}

Cellfunciona para todos los tipos, pero funciona mejor para los tipos simples Copy porque Cell no usa referencias. Tiene un método denominado get() que solo funciona en tipos Copy.

Otro tipo que se puede usar es RefCell.

RefCell

Un RefCell es otra forma de cambiar valores sin necesidad de declararlos mut. Es como Cell, pero utiliza referencias en lugar de copias.

El ejemplo a continuación permite ver cómo es similar a Cell.

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // Many other fields
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    println!("{:?}", user_1.active);
}

Este ejemplo imprime RefCell { value: true }.

RefCell tiene muchos métodos. Dos de ellos son .borrow() y .borrow_mut(). Con estos métodos, se puede hacer lo mismo que con & y &mut. Las reglas son las mismas:

  • Se pueden hacer muchos préstamos simultáneos.
  • Solo se puede hacer un préstamo modificable.
  • No se pueden hacer préstamos simultáneos de ambos tipos.

Modificar el valor de un RefCell es así de fácil:

#![allow(unused)]
fn main() {
// 🚧
user_1.active.replace(false);
println!("{:?}", user_1.active);
}

Dispone también de otros métodos como replace_with que utiliza un cierre (closure):

#![allow(unused)]
fn main() {
// 🚧
let date = 2020;

user_1
    .active
    .replace_with(|_| if date < 2000 { true } else { false });
println!("{:?}", user_1.active);
}
}

Es necesario prestar atención al uso de RefCell ya que valida si los préstamos son correctos en tiempo de ejecución. No en tiempo de compilación. Es decir, cuando el programa ya se está ejecutando. Por eso, cosas como esta compilarán, aunque sean erróneas:

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // Many other fields
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    let borrow_one = user_1.active.borrow_mut(); // primer préstamo modificable - correcto
    let borrow_two = user_1.active.borrow_mut(); // segundo préstamo modificable (sigue el primero) - incorrectos
}

Si se ejecuta el código anterior, Rust entrará en pánico:

thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:21:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\rust_book.exe` (exit code: 101)

Lo destacable del mensaje de error es already borrowed: BorrowMutError. Así que cuando se usan RefCell resulta conveniente ejecutar el código y pasar un conjunto de pruebas.

Mutex

Los Mutex posibilitan otra forma de modificar valores sin declararlos como mut. Significa exclusión mutua, lo que significa que "solo puede modificarlo un proceso cada vez". Por eso, el uso de mutex es seguro, porque permite la modificación interna, pero solo a un proceso cada vez. Es útil en la programación concurrente. Para ello, este tipo utiliza .lock(). Esta función es como cerrar la puerta con un candado desde dentro: se entra en una habitación, se cierra con llave y a partir de ahí, se puede modificar lo que contenga la habitación. Ningún otro proceso puede entrar a detener o contradecir los cambios.

Los mutex son más fáciles de explicar a través de ejemplos:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5); // Un nuevo Mutex<i32>. No se dice que sea mut
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer es un MutexGuard
                                // Esta veriable tiene que ser  mut para cambiarla
                                // Con ella se tiene acceso al contenido de Mutex
                                // Si se intenta imprimir el mutex:

    println!("{:?}", my_mutex); // Se imprime "Mutex { data: <locked> }"
                                // El mensaje indica que no es posible acceder al valor,
                                // está bloquedo y solo puede acceder mutex_changer
                                // mientras mantenga el bloqueo

    println!("{:?}", mutex_changer); // Esto sí funciona, imprime 5. Se cambiara a 6.

    *mutex_changer = 6; // mutex_changer es un MutexGuard<&i32> se usa * para cambiar el valor i32

    println!("{:?}", mutex_changer); // Ahora indica 6
}

¿Cómo se puede "reabrir la puerta"? ¿Cómo se puede liberar el bloqueo del valor que aún mantiene mutex_changer para que otro lo pueda usar? Es necesario que la variable MutexGuard salga fuera de ámbito. Que su código finalice. Por ejemplo:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    {
        let mut mutex_changer = my_mutex.lock().unwrap();
        *mutex_changer = 6;
    } // mutex_changer sale de ámbito - desaparece. Ya no está bloquedo el valor.

    println!("{:?}", my_mutex); // Ahora indica: Mutex { data: 6 }
}

Es necesario tener cuidado en la programación concurrente ya que se pueden producir interbloqueos debido a que un segundo intento de bloqueo sobre un mutex mientras está bloquedo por otra variable producirá la parada y espera del segundo intento. Sin programación concurrente, se puede ver en el siguiente ejemplo:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer tiene el bloqueo
    let mut other_mutex_changer = my_mutex.lock().unwrap(); // other_mutex_changer quiere bloquear
                                    // el programa se queda parado esperando
                                    // y esperando
                                    // y esperará para siempre.

    println!("Esto nunca se imprimirá...");
}

Existe otro método try_lock() que intenta obtener el bloqueo una vez y si no lo consigue abando. En este caso, no se debe usar try_lock().unwrap() ya que si no consigue el bloqueo, la aplicación entrará en pánico. En este caso no se puede renunciar a let o match para validar correctamente:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap();
    let mut other_mutex_changer = my_mutex.try_lock(); // intenta obtener el bloqueo

    if let Ok(value) = other_mutex_changer {
        println!("El  MutexGuard contiene: {}", value)
    } else {
        println!("No se pudo obtener el bloqueo")
    }
}

Tampoco es necesario crear una variable para modificar el valor:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    *my_mutex.lock().unwrap() = 6;

    println!("{:?}", my_mutex);
}

La línea de código *my_mutex.lock().unwrap() = 6; significa que *bloquea para mi uso el valor contenido en el mutex y conviértela en el 6". De esta forma, no existe variable y, por lo tanto, no es necesario desbloquear el mutex saliendo del ámbito de una variable. Esto se puede hacer tantas veces como se quiera:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    for _ in 0..100 {
        *my_mutex.lock().unwrap() += 1; // bloquea y desbloquea 100 veces
    }

    println!("{:?}", my_mutex);
}

RwLock

RwLock significa "bloqueo de lectura y escritura". Es como un Mutex, pero también como un RefCell. Se utiliza .write().unwrap() para modificarlo, en lugar de .lock().unwrap(). Y también se puede usar .read().unwrap() para obtener acceso de lectura. Es como RefCellya que sigue las reglas:

  • se pueden tener muchas varaibles .read().
  • se puede tener una sola variable .write().
  • no se puede tener a la vez una variable .write() con otras variables.

El programa se quedará bloqueado para siempre si se intenta .write() cuando no se puede obtener acceso.

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap(); // un .read() es correcto
    let read2 = my_rwlock.read().unwrap(); // dos.read() también es correcto

    println!("{:?}, {:?}", read1, read2);

    let write1 = my_rwlock.write().unwrap(); // uh oh, aquí el programa se ejecutará para siempre
}

Se debe liberar el bloqueo previo. Para ello se usa std::mem::drop. Igual que se podía hacer con mutex.

use std::sync::RwLock;
use std::mem::drop; // We will use drop() many times

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap();
    let read2 = my_rwlock.read().unwrap();

    println!("{:?}, {:?}", read1, read2);

    drop(read1);
    drop(read2); // descartamos las dos variables, para oder usar .write()

    let mut write1 = my_rwlock.write().unwrap();
    *write1 = 6;
    drop(write1);
    println!("{:?}", my_rwlock);
}

También se puede usar try_read() y try_write().

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap();
    let read2 = my_rwlock.read().unwrap();

    if let Ok(mut number) = my_rwlock.try_write() {
        *number += 10;
        println!("Ahora el núemro es {}", number);
    } else {
        println!("No se puede obtener acceso, lo siento.")
    };
}