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.
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.
- Crear una referencia compartida con
&y seguirla con el operador*. - Entender que
&Tconcede acceso de lectura sin mover la propiedad. - Ver por qué una referencia no ejecuta
drop: no posee nada que liberar. - Preferir
&stra&Stringen 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 `&`
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.
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.
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.
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.
- Escribe una función
fn primera_palabra(s: &str) -> usizeque devuelva la longitud hasta el primer espacio, y llámala con unStringy con un literal. - 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. - Cambia una firma que toma
&Stringpor otra que toma&stry observa que ahora también aceptas literales sin clonar. - Con
let r = &x; assert_eq!(*r, x);sobre uni32, explica qué hace*ry por quér.eq(&x)también funciona. - Explica en una frase por qué que
&TseaCopyes seguro, apelando a que la referencia no posee el dato.