Saltar al contenido principal

Referencias y borrowing

Borrowing: cómo se comparte la referencias de los datos.

Hasta este punto, gran parte del código que hemos visto presentaba una limitación importante: cuando pasábamos valores a una función, estos solían moverse (move), es decir, la función recibía la propiedad del dato.

Al transferir la propiedad, la variable original ya no podía usarse después de la llamada, a menos que se devuelva el valor como un dato de retorno de la función, lo cual resulta poco práctico en muchos casos.

Para resolver esto, Rust introduce el concepto de referencias, que nos permiten prestar acceso al valor sin ceder su propiedad. A la acción de crear referencias se le suele llamar borrowing (préstamo).

  • Una referencia es como un puntero: es una dirección que apunta a datos almacenados en memoria
  • Diferencia con punteros: se garantiza que una referencia siempre apunte a un valor válido durante su vida útil
  • El símbolo & representa referencias
  • Las referencias son inmutables por defecto (al igual que las variables)
fn main() {
let s1 = String::from("Hello"); // "s1" tiene la propiedad

// Pasamos una referencia, no movemos el valor
let len = calculate_length(&s1); // "&s1" crea una referencia

// "s1" sigue siendo válido aquí
println!("The length of '{s1}' is {len}.");
}

// La funcion recibe una referencia, "&", a un String
fn calculate_length(s: &String) -> usize {
s.len()

} // "s" sale del scope, pero NO se destruye el valor porque no es el owner
  • Se eliminó la tupla en la firma de la función ya que no es necesario devolver el resultado + la variable recibida
  • Se eliminó la desestructuración de los valores de retorno de la función ya que solo retorna el resultado
  • Se pasa una referencia de &s1 a la función y en su definición se recibe una referencia &String
  • Cuando las funciones tienen referencias como parámetros en lugar de los valores reales, no es necesario devolver los valores para devolver la propiedad ya que nunca se tuvo
  • El scope de validez del parámetro dentro de la función es el mismo que el de cualquier otro parámetro, pero el valor al que apunta la referencia no se descarta al dejar de usarse ya que no tiene la propiedad

Figura 1: Diagrama de una referencia de una cadena (&String s), apuntando al valor real (String s1)

Figura 6

Comparación: Ownership vs Borrowing

ConceptoOwnershipBorrowing
TransferenciaSe mueve el valorSe presta el valor
Variable originalSe invalidaSigue válida
PerformancePuede requerir copias/movesMás eficiente
Uso posteriorRequiere devolución explícitaAutomático
Nota

Lo opuesto a referenciar usando & es desreferenciar, usando el operador de desreferencia *.

Reglas de referencias Resumen de las referencias:

  • Se puede tener una referencia mutable o cualquier cantidad de referencias inmutables
  • Las referencias deben ser siempre válidas

Referencias mutables

Para poder modificar un valor prestado, es necesario pasarlo como referencia mutable (&mut).

fn main() {
let mut x: i32 = 10; // Variable mutable

let r = &mut x; // Referencia mutable

// Incrementa el valor de la variable original
// a través de la referencia mutable
*r += 1;

// Muestra la variable original,
// que ha sido modificada a través de la referencia mutable
println!("Variable x: {}", x);

// Esto no compila porque 'r' es una referencia mutable hacia "x"
// y ya fue usada anteriormente
// se podría usar antes de que se consuma.
println!("Variable r (referencia): {}", r);
}
fn main() {
let mut s = String::from("Hello"); // La variable original debe ser mutable

// Se pasa una referencia mutable con "&mut" en lugar de solo la referencia "&"
change(&mut s);

println!("{s}"); // "Hello, world"
}

// La funcion recibe una referencia mutable de tipo String (&mut String)
fn change(some_string: &mut String) {
some_string.push_str(", world!");
}

Requisitos para referencias mutables

  1. La variable original debe ser mutable (let mut s)
  2. La firma de la función debe recibir una referencia mutable (some_string: &mut String)
  3. Se pasa una referencia mutable a la función (&mut s)
  4. No retorno necesario: Los cambios se aplican directamente a la variable original
  5. Hace explícita la intención de que la función mutará el valor que toma prestado

El operador de desreferenciación (*)

El símbolo * permite acceder al valor real al que apunta una referencia, no a la referencia misma.

Siempre se necesita el operador *

  • Con tipos de datos primitivos
  • Con operadores aritméticos y lógicos
  • Para asignaciones directas (incluso con tipos complejos)
fn operate(num: &mut i32, flag: &mut bool, text: &mut String, v: &mut Vec<i32>) {
*num = *num + 1; // Primitivos necesitan *
*flag = !*flag; // Con operadores aritméticos y lógicos
*text = String::from("nuevo"); // Asignación directa requiere *
*v = vec![1, 2, 3]; // Asignación directa requiere *
}

Rust aplica deref coercion automática cuando:

  • Se llaman métodos (como .push_str())
  • El tipo implementa el trait Deref o DerefMut
  • En comparaciones (cuando el tipo lo permite)
fn modify_collections(s: &mut String, v: &mut Vec<i32>) {
s.push_str("texto"); // Sin * - desreferenciación automática
v.push(42); // Sin * - desreferenciación automática

// Internamente Rust hace
(*s).push_str("texto");
(*v).push(42);
}

Rust tiene reglas estrictas para prevenir data races (carreras de datos) en tiempo de compilación:

Restricción 1. Solo una referencia mutable a la vez

No se puede tener dos referencias mutables al mismo valor al mismo tiempo.

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

let r1 = &mut s;
let r2 = &mut s; // error[E0499]: cannot borrow `s` as mutable more than once at a time

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

El error indica que el código es incorrecto porque no es posible tomar prestado s como mutable más de una vez

  • El primer préstamo mutable (mutable borrow) está en nombre de r1 y debe durar hasta que se use esa referencia (que sería en la línea de println!)
  • Pero, entre la creación de esa referencia mutable y su uso, se intentó crear otra referencia mutable en r2 que toma prestados los mismos datos que r1

La ventaja de esta restricción es que Rust puede evitar las carreras de datos en tiempo de compilación. Una carrera de datos es similar a una condición de carrera y ocurre cuando se presentan estos tres comportamientos:

  • Dos o más punteros acceden a los mismos datos al mismo tiempo
  • Se está utilizando al menos uno de los punteros para escribir los datos
  • No se utiliza ningún mecanismo para sincronizar el acceso a los datos

Podemos usar llaves para crear un nuevo scope, lo que permite múltiples referencias mutables, pero no simultáneas:

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

{
let r1 = &mut s;
} // Aqui "r1" sale del scope,
// entonces podemos crear una nueva referencia sin problemas

let r2 = &mut s;

Restricción 2. No mezclar referencias mutables e inmutables

  • No se puede tener una referencia mutable si ya existe una inmutable.
let mut s = String::from("Hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
// error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

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

Los usuarios de una referencia inmutable no esperan que el valor cambie repentinamente. Sin embargo, se permiten múltiples referencias inmutables porque nadie que simplemente lea los datos puede influir en la lectura de los mismos por parte de otros.

Non-Lexical Lifetimes (NLL): El scope de una referencia no es todo su bloque léxico, sino desde su creación hasta su último uso: Por ejemplo, el siguiente código se compilara porque el último uso de las referencias inmutables está en el println!(), antes de que se introduzca la referencia mutable:

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

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Las variables "r1" y "r2" no se utilizaran despues de este punto

let r3 = &mut s; // no problem
println!("{r3}")
  • El scope de las referencias inmutables, r1 y r2, terminan después de println!, donde se usaron por última vez, es decir, antes de crear la referencia mutable r3
  • Estos alcances no se superponen, por lo que el el código es permitido, el compilador puede detectar que la referencia ya no se usa antes de que finalice el scope

Ejemplo de referencia Mutable e Inmutable

struct BankAccount {
owner: String,
balance: f64,
}

impl BankAccount {
// Constructor para crear una nueva cuenta
fn new(owner: String, initial_balance: f64) -> Self {
Self {
owner,
balance: initial_balance,
}
}

// Método para verificar el saldo de la cuenta
// Utiliza una referencia inmutable (&self) porque no modifica los datos
// Este método hace dos cosas: imprime el saldo Y lo retorna
fn check_balance(&self) -> f64 {
println!("Saldo de la cuenta de {}: ${:.2}", self.owner, self.balance);
self.balance
}

// Método para depositar dinero
// Toma una referencia mutable (&mut self) porque modifica el estado
fn deposit(&mut self, amount: f64) -> Result<(), String> {
if amount <= 0.0 {
return Err("El monto debe ser positivo".to_string());
}

println!("Depositando ${:.2} en la cuenta de {}", amount, self.owner);
self.balance += amount;
Ok(())
}
}

fn main() {
// Crear una instancia mutable de BankAccount usando constructor
let mut account = BankAccount::new("Lucas".to_string(), 100.0);

// FORMA 1: Capturar el valor retornado por check_balance()
// Esto permite usar el balance en cálculos o comparaciones posteriores
let current_balance = account.check_balance();
println!("Balance capturado para uso posterior: ${:.2}", current_balance);

// Referencia mutable para aumentar el saldo
match account.deposit(25.0) {
Ok(()) => println!("Depósito exitoso"),
Err(e) => println!("Error en depósito: {}", e),
}

// Balance final
// FORMA 2: Ejecutar check_balance() solo por su efecto secundario (imprimir)
// No capturamos el valor retornado, solo queremos mostrar el saldo
account.check_balance();
}

Dangling References

En lenguajes con punteros, es fácil crear erróneamente un puntero colgante, dangling reference, (es una referencia que apunta a memoria que ya no contiene datos válidos) al liberar memoria y conservar un puntero a dicha memoria. En Rust, el compilador garantiza que las referencias nunca serán colgantes: si se tiene una referencia a datos, el compilador se asegurará de que estos no salgan del scope antes de la referencia a ellos.

Ejemplo de referencia colgante (para forzar el error en tiempo de compilación):

fn main() {
let reference_to_nothing = dangle(); // Error de compilacion
}

fn dangle() -> &String { // la funcion retorna una referencia a un String

let s = String::from("Hello"); // "s" es un String recien creado

&s // Se retorna la rereferencia al String "s"

} // "s" sale del scope y se elimina, por lo que la referencia queda "colgando"
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|

El mensaje de error se refiere a la característica: lifetime (tiempo de vida). Sin embargo, si se ignora la información sobre los lifetime por ahora, la clave del problema es:

this function's return type contains a borrowed value, but there is no value for it to be borrowed from

El tipo de retorno de esta función contiene un valor prestado, pero no hay ningún valor del cual tomarlo prestado
  • Se está tratando de devolver una referencia a algo que ya no va a existir
  • Dado que s se creó dentro del scope de la función dangle(), cuando la función termine, los datos se borrarán, pero se está intentando devolver una referencia a ese valor.
  • Significa que la referencia estaría apuntando a un String no válido
  • Eso es un error, por lo que el compilador lanza un error
  • La solución es no devolver referencias de una función, sino que, devolver los valores directamente.

Esto funciona sin problemas. Se transfiere la propiedad (ownership) y no se desasigna (deallocated) nada.

// Se retorna la propiedad (String) en lugar de una referencia (&String)
fn no_dangle() -> String {
let s = String::from("Hello");

s
}

Se puede devolver referencias solo cuando los datos viven fuera de la función

fn get_first_word(text: &String) -> &str {
&text[0..5]
}

fn main() {
let text = String::from("Hello world");
let word = get_first_word(&text);
println!("{word}"); // imprime "Hello"
}