Una referencia es una forma de usar un valor sin tomar posesión de él.
En el artículo anterior vimos que si pasamos una variable compleja (como un String) a una función, le transferimos la propiedad (Ownership). La variable original muere y no podemos volver a usarla.
Esto es muy seguro, pero terriblemente incómodo. Imagina tener que devolver la variable al final de cada función solo para poder seguir usándola:
// Lo que tendríamos que hacer SIN referencias (¡Qué horror!)
fn main() {
let s1 = String::from("hola");
let (s2, len) = calcular_longitud(s1); // Tenemos que devolver s1 para no perderla
println!("La longitud de '{}' es {}.", s2, len);
}
fn calcular_longitud(s: String) -> (String, usize) {
let length = s.len();
(s, length) // Devuelve la String y la longitud
}¿No sería mejor poder “prestar” la variable a la función para que la lea, sin regalarle la propiedad? Eso es exactamente el Borrowing (Préstamo).
Referencias inmutables (&T)
Una referencia es como un puntero en C++, es una dirección de memoria que nos permite acceder a un dato que pertenece a otra variable. En Rust, creamos referencias usando el símbolo ampersand (&).
Al acto de crear una referencia lo llamamos Borrowing.
fn main() {
let s1 = String::from("hola");
// Pasamos &s1 (una referencia), no s1 (el valor)
let len = calcular_longitud(&s1);
// ¡s1 sigue siendo válida aquí! Porque solo la "prestamos".
println!("La longitud de '{}' es {}.", s1, len);
}
fn calcular_longitud(s: &String) -> usize { // s es una referencia a un String
s.len()
} // Aquí, 's' sale del ámbito. Pero como NO tiene el ownership, no pasa nada.
// El String original NO se borra.Visualmente, &s1 crea un puntero que apunta a la estructura de s1 (que a su vez apunta al Heap). No somos dueños del dato, solo tenemos permiso para mirarlo.
Por defecto, las referencias son inmutables. Si intentas modificar algo prestado con &, el compilador te dará un error. “Se mira, pero no se toca”.
Referencias mutables (&mut T)
¿Y si queremos prestar algo para que alguien lo modifique? (Por ejemplo, una función que añade texto a un String).
Necesitamos dos cosas:
- Que la variable original sea mutable (
mut). - Pasar una referencia mutable usando
&mut.
fn main() {
let mut s = String::from("hola");
cambiar(&mut s); // Pasamos una referencia mutable
println!("{}", s); // Imprime "hola mundo"
}
fn cambiar(some_string: &mut String) {
some_string.push_str(" mundo");
}¡Perfecto! Hemos modificado el dato sin tomar posesión de él. A partir de aquí, el Borrow Checker comprueba que las referencias no se solapen de forma incompatible.
Las reglas del borrowing
Para garantizar la seguridad de memoria y evitar aliasing mutable incompatible, Rust impone dos reglas sobre las referencias. Estas mismas reglas también impiden las data races cuando compartimos datos entre hilos mediante APIs seguras.
Regla 1: La Regla de la Convivencia
Mientras las referencias sigan en uso, puedes tener una de las siguientes opciones para el mismo valor, pero no ambas:
- Cualquier número de referencias inmutables (
&T). - Exactamente una referencia mutable (
&mut T).
Piénsalo como un documento de Google Docs:
- Modo Lectura: Muchas personas pueden leer el documento a la vez sin problema.
- Modo Edición: Si alguien está escribiendo, nadie más debería estar leyendo o escribiendo a la vez para evitar el caos.
Intento fallido: dos mutables a la vez
let mut s = String::from("hola");
let r1 = &mut s;
let r2 = &mut s; // ❌ ERROR: cannot borrow `s` as mutable more than once at a time
println!("{}, {}", r1, r2);Rust impide este aliasing. En código concurrente, permitir que dos accesos no sincronizados escribieran sobre la misma memoria podría producir una data race.
Intento fallido: Mezclar lectura y escritura
let mut s = String::from("hola");
let r1 = &s; // Lectura
let r2 = &s; // Lectura
let r3 = &mut s; // ❌ ERROR: cannot borrow `s` as mutable because it is also borrowed as immutable
println!("{}, {}, {}", r1, r2, r3);Si r1 está leyendo, no espera que el dato cambie bajo sus pies. r3 podría cambiar el contenido o incluso redimensionar el string (invalidando la memoria), dejando a r1 apuntando a la nada. Rust prohíbe esto.
El alcance de cada préstamo
Estas reglas aplican al tiempo de vida de la referencia. Si una referencia deja de usarse, podemos crear otra nueva.
Un bloque permite terminar de forma explícita el uso de una referencia antes de crear otra:
let mut s = String::from("hola");
{
let r1 = &mut s;
// r1 hace su trabajo aquí...
} // r1 muere aquí. El "bloqueo" de escritura se libera.
let r2 = &mut s; // ✅ Ahora sí podemos crear otra referencia mutable.Además, el compilador moderno de Rust es listo (gracias a una característica llamada Non-Lexical Lifetimes). Sabe cuándo dejas de usar una referencia.
let mut s = String::from("hola");
let r1 = &s;
println!("{}", r1);
// El compilador ve que r1 NO se usa más abajo.
// Por tanto, su "préstamo" termina aquí implícitamente.
let r2 = &mut s; // ✅ Esto funciona porque r1 ya no molesta.
r2.push_str(" mundo");Regla 2: Referencias siempre válidas (No Dangling References)
En lenguajes como C++, es fácil crear un puntero que apunta a una memoria que ya ha sido liberada (Dangling Pointer).
// Ejemplo en C++ (Peligroso)
string* crear() {
string s = "hola";
return &s; // 💀 Devuelve referencia a variable local que va a morir
}Si intentas hacer esto en Rust:
fn crear<'a>() -> &'a String {
let s = String::from("hola");
&s // ❌ ERROR: returns a reference to data owned by the current function
}Rust te detiene. La función promete una referencia con una vida elegida por quien llama, pero s va a destruirse al terminar la llamada. No existe ningún tiempo de vida válido para esa referencia.
La solución en este caso es devolver el String directamente (transferir el Ownership), no la referencia.