Primeros pasos en Rust - Ownership y Funciones

Apuntes de Rust - Parte 7

Como mencionamos en un artículo anterior, Rust es un lenguaje que tiene un sistema de ownership o propiedad estricto que garantiza la seguridad de la memoria y evita una condición de carrera. El ownership o la propiedad expresa que cada valor en Rust tiene una variable que es su propietario, y solo puede existir un propietario a la vez.

Cuando el propietario sale de ámbito, el valor se descarta y su memoria se libera. La mecánica de pasar un valor a una función es similar a la de asignar un valor a una variable. Pasar una variable a una función moverá o copiará, al igual que en una asignación.

Cuando pasas un valor a una función, estás transfiriendo la propiedad de ese valor al parámetro de la función. Esto significa que la variable original ya no puede utilizar ese valor después de la llamada a la función, porque ya no es propietaria. Esto se llama mover el valor.

Ejemplo:

fn main() {
    // cadena es el propietario del String "Hola Rust!"
    let cadena = String::from("Hola Rust!"); 
    // cadena se mueve al parámetro de función tomar_cadena
    tomar_cadena(cadena);
    // error: cadena ya no es el dueño del String
    println!("{}", cadena); 
}

fn tomar_cadena(una_cadena: String) {
    // una_cadena es el dueño del String
    println!("{}", una_cadena); 
}

En este ejemplo, la variable cadena es propietaria de un valor de String en la función principal. Cuando pasamos cadenaa la función tomar_cadena, transferimos la propiedad de la cadena al parámetro una_cadena de la función tomar_cadena. Esto significa que la variable cadena original ya no puede acceder ni modificar el String después de la llamada a la función. Si intentamos hacerlo, obtendremos un error en tiempo de compilación. Ahora, la función tomar_cadenaes propietaria de la cadena y puede usarla hasta que salga de ámbito y la descarte.

Pasar por referencia

Mover valores puede ser costoso si son grandes o complejos, como vectores o estructuras. También puede ser incómodo si queremos usar el mismo valor en varios lugares sin ceder la propiedad. Aquí es donde las referencias resultan útiles.

Una referencia es una forma de tomar prestado un valor sin adquirir la propiedad de este. Se crea una referencia utilizando el operador & en una variable, y apunta al valor que la variable posee.

Una referencia se puede pasar a una función como parámetro, y la función puede utilizar el valor a través de la referencia sin poseerlo. Esto significa que la variable original aún es propietaria del valor y puede usarlo después de la llamada a la función. A esto se le llama tomar prestado el valor.

Ejemplo:

fn main() {
    // cadena es el propietario del String "Hola Rust!"
    let cadena = String::from("Hola Rust!"); 
    // cadena es tomado prestado (borrowed) por el parámetro de función
    prestar_cadena(&cadena);
    // cadena continua como dueño del String
    println!("{}", cadena); 
}

fn prestar_cadena(una_cadena: &String) {
    // una_cadena tomo prestado el String
    println!("{}", una_cadena); 
}

Creamos una referencia a cadena utilizando &cadena y la pasamos a la función prestar_cadena. La función prestar_cadena toma una referencia como parámetro y puede usarla para acceder o leer el valor de la cadena. Sin embargo, no posee ni descarta la cadena cuando sale de ámbito. La cadena original aún posee y puede usar el String después de la llamada a la función.

Tomar prestado valores tiene algunas ventajas sobre mover valores: evita la copia o eliminación innecesaria de valores y nos permite usar valores en varios lugares sin ceder la propiedad. Sin embargo, tomar prestado valores también tiene algunas reglas que deben seguirse para garantizar la seguridad de la memoria:

  • Puedes tener ya sea una referencia mutable o cualquier número de referencias inmutables a un valor a la vez.

  • Las referencias siempre deben ser válidas; no pueden apuntar a memoria no asignada o liberada.

Estas reglas evitan una condición de carrera y acceso inválido a la memoria que podrían causar comportamientos indefinidos o fallos.

Pasar por referencia o pasar por valor en las funciones de Rust, son mecanismos diferentes de compartir datos entre funciones. Pasar valores transfiere la propiedad de los datos de una variable a otra, mientras que pasar referencias toma prestados los datos sin adquirir la propiedad. Pasar valores garantiza un acceso y control exclusivos sobre los datos, mientras que pasar referencias evita la copia o eliminación innecesaria de datos y permite un acceso compartido.

Pasar por valor

Pasar por valor a una función tiene sentido cuando deseamos transferir la propiedad de un valor a una función y no necesitamos que la función devuelva una nueva versión modificada del valor original. Esto es útil cuando el valor no se modificará y solo queremos utilizarlo dentro de la función para realizar una operación.

Ejemplo:

const PI: f64 = 3.14159;

fn calcular_volumen_esfera(radio: f64) -> f64 {
    (4.0 / 3.0) * PI * radio.powi(3)
}

fn main() {
    let radio = 5.0;
    let volumen = calcular_volumen_esfera(radio);
    println!("El volumen de la esfera es: {:.2}", volumen);
}

En este ejemplo, tenemos la función calcular_volumen_esfera que toma el radio de una esfera por valor y calcula el volumen utilizando la fórmula matemática correspondiente. La función devuelve el volumen calculado como un número de punto flotante (f64).

Se pasa el valor del radio a la función por valor y no por referencia, dado que este valor no se utiliza posteriormente y solo se requiere para el cálculo del volumen en la función.

Otro ejemplo:

fn sumar(a: i32, b: i32) {
    let resultado = a + b;
    println!("El resultado de sumar {} y {} es: {}", a, b, resultado);
}

Referencias Mutables

En conclusón cuando las funciones tienen referencias como parámetros en lugar de los valores reales, no necesitamos devolver los valores para devolver la propiedad, porque nunca tuvimos la propiedad.

¿Qué sucede si intentamos modificar algo que estamos tomando prestado? Analicemos el siguiente bloque de código, que por cierto, si trata de ejecutarlo este no funciona.

fn main() {
    let cadena = String::from("Hola");
    modificar(&cadena);
} 

fn modificar(una_cadena: &String) {
    una_cadena.push_str(" Rust!");
}

Por qué no funciona? Simple, al igual que las variables son inmutables de forma predeterminada, también lo son las referencias. No se permite modificar algo a lo que tenemos una referencia.

Para solucionar el problema se debe especificar que es una referencia mutable.

fn main() {
    let mut cadena = String::from("Hola");
    modificar(&mut cadena);
    println!("{}", cadena); 
} 

fn modificar(una_cadena: & mut String) {
    una_cadena.push_str(" Rust!");
}

Primero cambiamos cadena a ser mutable (mut). Luego creamos una referencia mutable con &mut cadena donde llamamos a la función modificar, y actualizamos la firma de la función para aceptar una referencia mutable con una_cadena: &mut String. Esto deja en claro que la función modificar modificará el valor que toma prestado.

Las referencias mutables tienen una gran restricción: si tiene una referencia mutable a un valor, no puede tener otras referencias a ese valor. Este código que intenta crear dos referencias mutables a cadena, generando un error.

let mut cadena = String::from("Hola Rust!");
let cad1 = &mut cadena;
let cad2 = &mut cadena;
println!("{cad1}, {cad2}");

Este código es inválido porque no podemos tomar prestado cadena como mutable más de una vez al mismo tiempo. Esta restricción puede prevenir una condición de carrera en tiempo de compilación.

Referencias

  • Este post es un resumen en español del capítulo 4 del libro "The Rust Programming Language. 2nd Edition by Steve Klabnik and Carol Nichols, with contributions from the Rust Community. 2023"

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!