Borrowing en Rust y referencias – Aprende Rust (7/x)

En el post anterior vimos la primera parte para conocer el Onwership en Rust. Vimos un poco del uso de la memoria, el tipo String y las bases para saber cómo una variable funciona internamente y lo que podemos o no hacer con ellas. En este post avanzaremos un poco con references y borrowing. Dos conceptos muy importante para complementar este capítulo de ownership: referencias y borrowing en Rust.

Retomemos el bloque de código final del post anterior:

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

El detalle con este código, es que para retomar el valor de s1, que es hello, debemos regresar una tupla con dos valores: el valor original y la longitud de la cadena. Recordemos, que en el momento en que enviamos s1 a calculate_length, la variable s1 ya no existe, porque pasamos el ownership a la función y cuando esta ya ha terminado su ciclo, desaparece.

Para resolver esto, podemos usar las referencias en Rust (references). Modifiquemos un poco el código para entenderlo:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Primero modificamos la función, que ya no recibe un valor, sino una reference. Ya no vamos a recibir un String sino un &String.

Una reference es una forma de llamar a un valor sin tener que pasar el ownership a otra variable.

borrowing en rust
Imagen 1

En este diagrama, vemos que el valor de s es el ptr de s1. Es decir, s usa una referencia para que, a través de ella, puedas acceder al valor de s1. De esta forma, cuando s sale del scope en la función calculate_length, se elimina, pero s1 sigue viva en main.

En la siguiente porción de código creamos una referencia de s1:

let len = calculate_length(&s1);

Y recibimos esa referencia mediante el tipo &String:

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // s sale el scope. Pero como no tiene el ownership, no ejecuta el drop

Por el momento, solo hemos usado el valor de una referencia. ¿Pero qué pasa si queremos modificarla? Pues en Rust no se puede así como así.

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

Este código, aunque parece válido en otros lenguajes. En Rust nos marcará un error:

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference

Así como las variables, que por default son inmutables, también los son las referencias. Este concepto es llamado borrowing, porque como en la vida real, cuando pides algo, lo usas y lo regresas como estaba (debería ser lo ideal :P), pero no puedes modificarlo.

Referencias mutables

Podemos modificar una referencia borrowed haciendo lo siguiente:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Como vemos, ahora tenemos la línea de código:

change(&mut s);

Creamos la referencia modificable con &mut. Que aparece igual en la declaración de la función change:

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Con este cambio, ya podemos modificar el valor al que hacemos referencia. Lo que nos regresaría hello, world.

Sin embargo las referencias mutables tienen una restricción:

Si ya tienes una referencia mutable no puedes tener otra

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

Error:

error[E0499]: cannot borrow `s` as mutable more than once at a time

Nos dice claramente que solo puede haber una referencia mutable a la vez. Pero esto solo con las mutables, el siguiente código, por ejemplo, sería válido:

    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;

    println!("{}, and {}", r1, r2);

Aquí no hay problema, porque solo vamos a usar el valor, sin modificarlo.

¿Y si combinamos referencias mutables e inmutables?

    let mut s = String::from("hello");

    let r1 = &s; // ok
    let r2 = &s; // ok
    let r3 = &mut s; // 🙁

    println!("{}, {}, and {}", r1, r2, r3);

Error:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

El error surge porque si estamos usando una referencia inmutable, no esperamos que se vaya a modificar, porque daría resultados inesperados. Por eso debemos darles un término:

    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // las variables r1 and r2 ya no se usarán a partir de aquí

    let r3 = &mut s; // ok
    println!("{}", r3);

Aquí los scopes no se solapan, así que pueden interactuar dentro del main sin mayor problemas.

Con este breve post, terminamos el tema de Ownership y Borrowing en Rust.

Espero que les haya ayudado a entender mejor estas cositas que tiene Rust.

Gracias por leer.


Posted

in

, , ,

by