Referencias modificables (mutables)

Puedes ver este capítulo en YouTube en inglés

Si se necesita modificar un valor a través de una referencia, se debe indicar que la referencia sea modificable (mutable). Para ello, se utiliza &mut en lugar de &.

fn main() {
    // no hay que olvidar que hay que escribir mut en la variable original
    let mut mi_numero = 8;
    let num_ref = &mut mi_numero;
}

mi_numero es de tipo i32, y num_ref es de tipo &mut i32 (es una "referencia modificable/mutable a un valor ì32).

Si se desea usar esta referencia para sumar 10, no se puede usar num_ref += 10 ya que num_ref no es de tipo i32, es &i32. Para obtener el valor de la referencia, se debe usar * que significa que "no se necesita la referencia, sino el valor que al que representa". En otras palabras, * es lo opuesto a &, un * borra a un &.

fn main() {
    let mut mi_numero = 8;
    let num_ref = &mut mi_numero;
    *num_ref += 10; // Usa * para cambiar el valor i32
    println!("{}", mi_numero);

    let segundo_numero = 800;
    let triple_referencia = &&&segundo_numero;
    println!("segundo_numero = ¿triple_referencia? {}",
        segundo_numero == ***triple_referencia);
}

El código anterior, da como resultado:

18
segundo_numero = ¿triple_referencia? true

El uso del operador sobre una variable & se denomina "referenciar". El uso del operador sobre una variable de referencia * se denomina "desreferenciar".

Rust usa dos reglas para las referencias mutables e inmutables. Son muy importantes y fáciles de recordar porque tienen sentido.

  • Regla 1: Si solo existen referencias inmutables a un valor, se pueden tener tantas como se quiera.
  • Regla 2: Si existe una referencia mutable a un valor, solo puede existir una referencia. Esto último significa que no pueden existir a la vez referencias inmutables y mutables en el mismo momento.

Estas reglas son necesarias debido a que las referencias mutables pueden cambiar los datos. Sería problemático que se modificara un dato cuando otras referencias lo están usando.

Un forma de entenderlo es pensar en la creación de una presentación de Powerpoint 1.

1

No se trata de un ejemplo excesivamente bueno, ya que actualmente es posible editar simultáneamente en Office 365.

El primer caso representa el de la existencia de una referencia mutable: un empleado está editando una presentación de Powerpoint. Piode ayuda a su jefe. El empleado entrega sus credenciales de acceso a su jefe para que pueda editar el Powerpoint. Ahora el jefe tiene una "referencia mutable" a la presentación de su empleado. El jefe puede hacer los cambios que quiera y devolver el ordenador más tarde. Nadie más tiene acceso a la presentación, por lo que este caso no da problemas.

El segundo caso representa el de **la existencia únicamente de referencias inmutables": El empleado entrega la presentación a 100 personas. Todas ellas pueden verla. Tienen una "referencia inmutable" a la presentación ya que nadie puede modificarla, por lo que este caso no da problemas.

El tercer caso representa la situación problemática a evitar: El empleado entrega sus credenciales de acceso a su jefe, que a partir de aquí dispone de una "referencia mutable". Además, el empleado entrega la presentación a 100 personas. El jefe puede entrar a editar y estas modificaciones pueden verse o no por parte de las 100 personas, según el momento en que accedan sin que puedan controlar.

Se puede ver que el intento de usar una referencia mutable simultáneamente con una inmutable no es aceptado por el compilador:

fn main() {
    let mut numero = 10;
    let numero_ref = №
    let numero_modif = &mut numero;
    *numero_modif += 10;
    println!("{}", numero_ref); // ⚠️
}

El compilador muestra un mensaje explicativo muy claro para mostrar el problema:

error[E0502]: cannot borrow `numero` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:24
  |
3 |     let numero_ref = №
  |                      ------- immutable borrow occurs here
4 |     let numero_modif = &mut numero;
  |                        ^^^^^^^^^^^ mutable borrow occurs here
5 |     *numero_modif += 10;
6 |     println!("{}", numero_ref); // ⚠️
  |                    ---------- immutable borrow later used here

Sin embargo, este código sí funciona. ¿Por qué?

fn main() {
    let mut numero = 10;
    let numero_modif = &mut numero; // referencia modificable
    *numero_modif += 10; // suma 10
    let numero_ref = № // referencia inmutable
    println!("{}", numero_ref); // imprime el valor referenciado
}

Imprime 20 sin problemas. Funciona porque el compilador es suficientemente inteligente para comprender el código. Conoce que se ha cmabiado numero a través de la referencia numero_modif, pero después, esta referencia mutable no se vuelve a usar. Por eso no hay problema aquí. No se usan "a la vez" referencias inmutables y mutables (como sí pasaba en el caso anterior).

En versiones anteriores de Rust, este código sí daba error, pero actualmente no lo hace ya que el compilador es más inteligente. Puede entender no solo lo que se ha tecleado, sino como se usa todo.

Ocultación (shadowing) de nuevo

Es necesario recordar que el ocultamiento de variables no destruye sus valores, sino que los bloquea. Con el uso de las referencias esto se ve más claro.

fn main() {
    let pais = String::from("Austria");
    let pais_ref = &pais;
    let pais = 8;
    println!("{}, {}", pais_ref, pais);
}

¿Qué imprime este código? ¿Austria, 8 o 8, 8? Imprime Austria, 8. En la primera línea, se declara una variable String con el valor Austria y denominada pais. En la segunda línea se crea una referencia al valor de esta variable. Es decir, a la String que contiene Austria. Después, se oculta la variable pais con una nueva cuyo valor es 8 y que es de tipo i32. El primer elemento pais no se destruyó (lo hará al finalizar el ámbito en el que está, en la llave de cierre de este bloque), por lo que sigue accesible a través de la referencia pais_ref. Para mayor claridad, se vuelve a mostrar el código, ahora con comentarios sobre su comportamiento.

fn main() {
    let pais = String::from("Austria"); // String denominada pais
    let pais_ref = &pais; // pais_ref es una referencia al valor
    let pais = 8; // nueva variable pais con un valor 8. Sin relación con la anterior, ni con pais_ref
    println!("{}, {}", pais_ref, pais); // pais_ref sigue "apuntando" al dato Austria
}