wandres.dev
BORROWING · referencias y reglas

Prestar en vez de mover: la referencia compartida &T

La referencia compartida `&T` concede acceso de solo lectura sin transferir la propiedad. Es la salida al patrón incómodo del nivel 8: dejar que una función mire un valor sin arrebatárselo a su dueño.

⏱ 15 min

El nivel 8 terminó con un dilema: para que una función leyera un valor no-Copy, había que cedérselo, y para recuperarlo, devolverlo en una tupla. La referencia compartida &T disuelve ese falso dilema. Es un puntero que concede acceso de lectura sin transferir la propiedad: la función mira el dato, pero el dueño sigues siendo tú. Este es el primer acto del borrowing, la mecánica que vuelve usable al ownership sin renunciar a una sola de sus garantías.

🎯 Al terminar esta lección sabrás
  • Crear una referencia compartida con & y seguirla con el operador *.
  • Entender que &T concede acceso de lectura sin mover la propiedad.
  • Ver por qué una referencia no ejecuta drop: no posee nada que liberar.
  • Preferir &str a &String en las firmas gracias a la coerción de deref.

El dilema falso del nivel 8

Recordemos el problema. Una función que solo quiere mirar un String tenía dos malas opciones: apropiárselo por valor (y devolverlo para que el dueño no lo perdiera) o clonarlo (y pagar una copia entera del heap). Ambas confunden dos cosas distintas: acceder a un dato y poseerlo. La referencia compartida las separa. Con & prestas el valor: la función accede sin poseer.

fn mide(s: &String) -> usize { // `&String`: toma prestado, no posee
    s.len()
}

fn main() {
    let cuenta = String::from("saldo pendiente");
    let n = mide(&cuenta);         // le PRESTAMOS `cuenta`, no se la damos
    println!("{cuenta} mide {n}"); // `cuenta` sigue siendo nuestra: no hubo move
}

El & aparece dos veces con papeles complementarios: en el tipo del parámetro (&String) declara “recibo un préstamo, no un dueño”; en la llamada (&cuenta) crea la referencia que se presta. Como no hay transferencia de propiedad, cuenta sobrevive intacta a la llamada.

🎁

Mover · ceder la propiedad

Pasar por valor. La función se queda el dato y tú lo pierdes salvo que te lo devuelva. Solo cuando de verdad cedes.

🤝

Prestar · conceder acceso

Pasar &valor. La función lo lee un rato; tú sigues siendo el dueño y lo conservas intacto tras la llamada.

Crear una referencia y seguirla: & y *

&x construye una referencia a x: un puntero que el compilador garantiza siempre válido, alineado y apuntando a un valor vivo e inicializado del tipo correcto. El operador inverso, *r, la desreferencia: sigue el puntero hasta el valor.

let x: i32 = 10;
let r: &i32 = &x;    // `r` apunta a `x` sin poseerla
assert_eq!(*r, 10);  // `*r` sigue el puntero hasta el valor
assert_eq!(r, &10);  // comparar referencias también funciona

En la práctica desreferencias poco a mano, porque el operador punto lo hace por ti. Al escribir r.len(), el compilador inserta el * necesario: r.len() es azúcar de (*r).len(). Ese autoderef es la razón por la que las referencias se leen igual de bien que el valor original.

let saludo = String::from("hola mundo");
let r = &saludo;
println!("{}", r.len());        // r.len() == (*r).len(): el punto autodereferencia
println!("{}", r.to_uppercase()); // los métodos de String funcionan a través de `&`
ℹ️
La referencia de Rust no es el puntero de C

Un &T se compila al mismo puntero de máquina que un T* de C, pero carga garantías que el puntero crudo no tiene: nunca es nulo, siempre está alineado y siempre apunta a un valor vivo del tipo correcto. Esas tres promesas no cuestan un byte en ejecución; las demuestra el compilador antes de emitir el binario. Por eso en Rust no existe el NULL dereference ni el puntero a basura: no son bugs que se eviten con disciplina, son estados que el tipo &T no puede representar.

Prestar no consume ni libera

Una referencia no es dueña de nada, y de ahí salen dos consecuencias que definen el préstamo compartido. La primera: cuando una referencia sale de scope, no se ejecuta ningún drop, porque no hay recurso suyo que liberar. La segunda: puede haber muchas referencias compartidas al mismo dato a la vez, sin conflicto, porque ninguna es responsable de liberarlo.

let datos = vec![10, 20, 30];
let a = &datos;
let b = &datos;   // varias referencias compartidas a la vez: permitido
let c = a;        // `&T` es Copy: se copia el puntero, no el Vec
println!("{}", a.len() + b.len() + c.len()); // 9
// mueren `a`, `b`, `c`: no liberan nada. Al final muere `datos` y libera su buffer

Fíjate en let c = a: como &T implementa Copy, asignar una referencia duplica el puntero, no el Vec que hay detrás. Copiar un &T es inofensivo precisamente porque ninguna copia posee el dato: da igual cuántos punteros de lectura apunten al mismo buffer, ninguno lo va a liberar.

flowchart LR
a[Referencia compartida a] -->|solo apunta| s[Variable duena del Vec]
b[Referencia compartida b] -->|solo apunta| s
s -->|posee y libera| buf[Buffer en el heap]

Hay algo que una referencia compartida no te deja hacer: escribir. A través de un &T solo se lee; cualquier intento de mutar el dato prestado es un error de compilación. Esa es, precisamente, la línea que la próxima lección cruzará con la referencia mutable.

let mut saldo = 100;
let r = &saldo;      // préstamo compartido: solo lectura
// *r += 1;          // ERROR E0594: cannot assign to `*r`, which is behind a `&` reference
println!("{r}");     // leer, siempre; escribir, jamás

El compilador responde con E0594. No es una restricción arbitraria: es la garantía que hace seguro repartir la referencia entre muchos lectores a la vez. Si uno solo de ellos pudiera escribir, los demás verían el dato cambiar bajo sus pies, y volveríamos al mundo de los bugs que Rust promete cerrar.

&str: prestar una vista, no el String

Hay un salto de calidad que separa al principiante del programador idiomático: escribir &str donde el principiante escribe &String. Un &str es una vista compartida sobre bytes UTF-8 que viven en otra parte —el buffer de un String, un literal incrustado en el binario, una porción de otro texto—. Es el préstamo de texto más general que existe.

fn saluda(nombre: &str) {   // `&str`: acepta cualquier vista de texto
    println!("Hola, {nombre}");
}

fn main() {
    let dueno = String::from("Ada");
    saluda(&dueno);   // `&String` se convierte en `&str` por coerción de deref
    saluda("Turing"); // un literal ya es `&str`, sin conversión
}

La conversión de &String a &str la hace sola la coerción de deref: como String sabe desreferenciarse a str, un &String encaja donde se pide un &str. El efecto es que una firma que toma &str acepta más llamadores que una que toma &String, sin coste alguno. La regla idiomática: presta la vista más general que baste.

💡
Presta el tipo más general: &str, no &String; &[T], no &Vec

La misma lógica gobierna las colecciones: acepta &[T] en vez de &Vec<T> y tu función servirá también a arrays y a slices de otros contenedores. Prestar el tipo-vista (&str, &[T]) en lugar del tipo-dueño (&String, &Vec<T>) no restringe nada y amplía a quién puedes servir. Es la manifestación práctica de un principio general: en la firma, pide el mínimo poder que necesites sobre el dato.

Guardar y devolver referencias

Una referencia es un valor de primera clase: cabe en una variable, se pasa a otra función y hasta se devuelve, con una única atadura —el dato al que apunta debe vivir más que ella—. Por eso una función puede recibir varias referencias y devolver una de ellas sin copiar absolutamente nada:

fn mas_larga<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }   // devolvemos una de las prestadas
}

fn main() {
    let uno = String::from("corto");
    let dos = String::from("bastante más largo");
    let elegida = mas_larga(&uno, &dos);   // `uno` y `dos` siguen vivos
    println!("la más larga mide {}", elegida.len());
}

La anotación 'a empareja la vida de la salida con la de las entradas: la referencia devuelta no puede sobrevivir a los datos de los que sale. No te detengas todavía en la sintaxis de los lifetimes —son el nivel siguiente—; basta con observar que prestar sigue siendo gratis incluso cuando una función elige y devuelve una referencia entre varias, sin clonar ni un byte del heap.

Prestar separa dos capacidades que otros lenguajes funden en una

La idea profunda de esta lección no es la sintaxis del &, sino la disección conceptual que habilita. En la mayoría de lenguajes, tener una variable significa a la vez poseer el objeto (ser responsable de su vida) y poder tocarlo (leerlo o escribirlo); las dos capacidades vienen soldadas. Rust las corta por la mitad. La propiedad —la responsabilidad de ejecutar el drop— se queda quieta en el dueño; el acceso se concede aparte, por un tiempo acotado, mediante una referencia. Una &T es entonces una capability con una firma precisa: acceso de lectura, no exclusivo, temporal, sin propiedad. Y como es una capacidad y no una posesión, se puede repartir libremente: mil lectores no se estorban, porque ninguno puede liberar ni mutar el dato bajo los pies de los demás. Cuando interiorices que &T significa “puedo mirar esto un rato, pero no es mío ni lo voy a cambiar”, el resto del borrowing —la exclusividad de &mut, las reglas del checker, los lifetimes— dejará de parecer una lista de restricciones y se revelará como lo que es: el álgebra de repartir accesos sin repartir responsabilidades.

📝
Lo esencial del préstamo compartido

Prestar con & concede acceso de lectura sin mover la propiedad: el dueño sobrevive a la llamada. Una referencia no posee nada, así que no libera al morir y puede coexistir con cuantos lectores quieras. A través de un & no se muta. Y en las firmas, presta la vista más general —&str, &[T]— para servir al máximo de llamadores sin coste alguno.

⚔️ Presta en vez de mover
  1. Escribe una función fn primera_palabra(s: &str) -> usize que devuelva la longitud hasta el primer espacio, y llámala con un String y con un literal.
  2. Crea un String, préstalo a dos referencias a la vez y comprueba que ambas conviven y que el dueño sigue vivo al final.
  3. Cambia una firma que toma &String por otra que toma &str y observa que ahora también aceptas literales sin clonar.
  4. Con let r = &x; assert_eq!(*r, x); sobre un i32, explica qué hace *r y por qué r.eq(&x) también funciona.
  5. Explica en una frase por qué que &T sea Copy es seguro, apelando a que la referencia no posee el dato.