Rust: Ownership && Borrowing


Ownership y borrowing son dos sistemas fundamentales sobre la gestión de memoria en Rust. Permiten gestionar los recursos de manera efectiva sin costo de abstracción, eliminando los errores más comunes sin tener la necesidad de utilizar un garbage collector (recolector de basura).

1. ¿Qué es el Ownership?

El sistema de Ownership indica que cada valor tiene un único dueño y solo puede haber un dueño a la vez, de lo contrario dará un error en tiempo de compilación.

fn main() {
    let n1 = String::from("test"); // `n1` es el dueño de la cadena "test".
    println!("{}", n1);

    let n2 = n1; // `n1` transfiere su ownership a `n2`.

    // println!("{}", n1); // Si intentamos imprimir n1 Rust nos dará 
    // un error en compilación, ya que el valor de n1 ahora pertenece a n2.

    println!("{}", n2); // Al intentar imprimir n2 no habría ningún problema.
}

Este sistema aplica solamente para los valores almacenados en el heap, con los valores almacenados en el stack no hay ningún problema. Articulo sobre el heap y el stack.

fn main() {
    let n1 = 5;
    let n2 = n1;

    println!("{}", n1);
    println!("{}", n2); // No hay problemas de Ownership al estar los datos en el stack
}

En este código se puede utilizar n1 y n2 sin problemas ya que n1 tiene por defecto un tipo de dato i32 y se almacena en el stack.

Para solucionar el problema de propiedad del ownership en el heap se puede utilizar el método clone(), esto realiza una copia de un valor en una nueva ubicación de la memoria, lo que permite volver a manejar este dato sin problemas en tiempo de compilación y ejecución.

fn main() {
    let n1 = String::from("test");
    println!("{}", n1);

    let n2 = n1.clone(); // Crea una copia de n1 en n2 con el metodo clone().

    println!("{}", n1); // Se pueden usar las dos variables sin problemas.
    println!("{}", n2);
}

2. ¿Cómo funciona el scope?

El scope (alcance) define la visibilidad y el tiempo de vida de las variables en el programa, se define con las { } creando un bloque de código. Al salir del scope se libera toda la memoria del mismo, dentro del scope se pueden utilizar variables que están por fuera del scope.

fn main() {
    // Scope principal
    let s1 = 2;
    {
        // Scope interno
        let s2 = 10;  // s2 solo existe dentro de este bloque
        println!("s1: {}, s2: {}", s1, s2);
    } // Se libera la memoria y ya no se puede utilizar s2

    println!("s1: {}", s1);  // Esto funciona
    // println!("s2: {}", s2);  // Esto daría error de compilación porque esta fuera del scope.
}

Ejemplo de como funciona el Ownership con scopes:

fn main() {
    let t1 = String::from("test");
    {
        let t2 = t1;
        println!("t2: {}", t2);
    }
    //println!("t1: {}", t1); Error de compilacion.
}

Intentar imprimir el valor de t1 al final del código daría un error de compilación, y aunque se libere la memoria al final del scope la propieda de t1 ya fue transferida a t2 y esto es irreversible.

Ejemplo de scopes anidados:

fn main() {
    let x = String::from("house");  // Principal scope

    {  // Segundo scope
        let y = String::from("car");

        {  // Tercer scope
            let z = String::from("dog");
            println!("x: {}, y: {}, z: {}", x, y, z); // Se puede acceder a las 3 variables
        }
        println!("x: {}, y: {}", x, y); // Se puede acceder a 2 variables ya que z(dog) fue liberada
    }

    println!("x: {}", x); // Solamente se puede acceder a la variable del scope principal (house)
}

3. ¿Qué es el Borrowing?

Borrowing es el sistema que se encarga de ofrecer "préstamos" permitiendo utilizar valores sin transferir su propiedad mediante las referencias, las cuales utilizan el símbolo &. Usar referencias es mucho más óptimo que usar el método clone(), por qué la referencia guarda solamente la dirección de memoria donde está guardado el valor de la variable que se le indica.

Las referencias en rust son similares a los punteros en c/c++ pero contienen algunas diferencias.

Referencias (Rust) Punteros (c/c++)
Nunca es nulo Puede ser nulo
No puede ser reasignado Puede ser modificado
Verificación en tiempo de compilación Sin garantías de seguridad

Explicación más detallada sobre el tema: Referencias vs punteros.

4. Tipos de borrowing

Existen dos tipos de borrowing inmutable y mutable. Cuando se hace un borrowing inmutable, se pasa una referencia a un valor sin modificarlo. Esto permite que varias partes del código puedan leer el valor al mismo tiempo, pero no pueden modificarlo.

fn custom_print(m: &str) { // Se debe indicar en el parametro de la funcion que se toma una referencia
    println!("El mensaje es: {}", m);
}

fn main() {
    let s1 = String::from("¡Hola, Rust!");
    let s2 = &s1; // Se crea una referencia que tiene la direccion del valor s1
    println!("s1: {}, s2: {}", s1, s2); // Se pueden leer las dos variables a la vez sin problemas
    custom_print(s2);
}

Por otro lado el mecanismo de borrowing mutable pasa una referencia que permite modificar el valor. Sin embargo, en este caso, solo puede existir una referencia mutable a un valor a la vez. Esto garantiza que no haya conflictos o condiciones de carrera durante el acceso y modificación de los datos.

fn main() {
    let mut s1 = String::from("¡Hola, Rust!");
    let s2 = &mut s1;
    // println!("s1: {}, s2: {}", s1, s2); Error de compilación
    println!("s2: {}", s2);
}

Este código demuestra que solo se puede acceder a s1 mediante s2 para no generar condiciones de carrera. Rust siempre da prioridad de acceso al valor de la última referencia realizada (se puede ver en el código de abajo).

fn main() {
    let mut s1 = String::from("¡Hola, Rust!");
    let s2 = &mut s1;
    let s3 = &mut s1;
    // println!("s2: {}, s3: {}", s2, s3); Error de compilación 
    // (no se puede utilizar s2 porque no es la última referencia)

    println!("s3: {}", s3); // Esta línea de código compila sin problemas,
    //ya que llama a la última referencia de s1
}

5. Modificando valores con referencias

Ahora veremos cómo modificar estas variables referenciadas utilizando la desreferencia. Algo a detallar es que solo se necesita usar la desreferencia con los tipos de datos primitivos (i32, f64, bool, char, etc.). Los tipos de datos más complejos (String, Vec, Struct, Box, etc.) no necesitan utilizar la desreferencia para modificar los datos en una referencia mutable porque esto se aplica automáticamente.

En este caso, s2 hace una referencia mutable de s1, que contiene un tipo de dato primitivo i32, luego se hace una desreferencia de s2 con el símbolo de * para poder modificarlo y, por último, se imprime el valor de s1 modificado.

fn main() {
    let mut s1 = 50;
    let s2 = &mut s1;

    *s2 += 30; // Desreferencia de s2 para modificar el valor
    println!("s1: {}", s1); 
}

También se puede realizar esto de esta forma, llamando a una función.

fn mod_ref(x: &mut i32) {
    *x +=30;
}

fn main() {
    let mut s1 = 50;
    mod_ref(&mut s1);

    println!("s1: {}", s1); 
}

Como antes mencionamos, para los tipos de datos más complejos (String, Vec, Struct, Box, etc.) no vamos a requerir indicarle una desreferencia para modificar los datos.

fn main() {
    let mut s1 = String::from("Hola");
    let s2 = &mut s1;

    s2.push_str(", mundo!"); // No hace falta hacer la desreferencia
    println!("s1: {}", s1); 
}

Un ejemplo igual, pero con un vector.

fn main() {
    let mut v1 = vec![1, 2, 3];
    let v2 = &mut v1;

    v2.push(4);  // Modificando el vector a través de v2
    println!("v1: {:?}", v1);  // Imprimiendo el vector modificado
}

6. Conclusiones

El sistema de gestión de memoria de Rust, basado en ownership y borrowing, nos ofrece una manera segura y eficiente de manejar la memoria sin la necesidad de un recolector de basura y sin impactar en el tiempo de ejecución del programa. Las reglas estrictas sobre las referencias y la propiedad nos garantizan que no haya problemas de concurrencia, uso de memoria después de liberarla, ni fugas de memoria. Al mismo tiempo, Rust nos permite un control preciso sobre la memoria en tiempo de compilación, lo que resulta en programas más rápidos y seguros.