En esta página
References y borrowing: usar sin poseer
References: usar sin poseer
En la lección anterior vimos que pasar un String a una función transfiere su ownership. Esto es correcto pero verboso: tienes que retornar el valor para seguir usándolo. Las references (referencias) son la solución.
Una referencia te permite referirte a un valor sin tomar su ownership. Se crea con el operador & y se denomina "borrowing" (préstamo) — estás tomando prestado el valor sin poseerlo.
fn imprimir_longitud(s: &String) -> usize {
// s es una referencia a String
// Podemos leer s, pero no somos sus dueños
println!("El texto es: {s}");
s.len()
}
fn main() {
let mi_string = String::from("Rust es genial");
// Pasamos una referencia — no movemos el ownership
let longitud = imprimir_longitud(&mi_string);
// mi_string sigue siendo válido aquí
println!("'{mi_string}' tiene {longitud} caracteres");
}El parámetro &String en la función indica que recibe una referencia. Cuando s sale de scope al terminar la función, no se llama drop() porque la función no tiene ownership.
Referencias inmutables: &T
Una referencia inmutable (&T) permite leer el valor pero no modificarlo:
fn main() {
let numero = 42;
let referencia = №
// Acceder al valor a través de la referencia (desreferenciación automática)
println!("Número: {numero}");
println!("A través de referencia: {referencia}");
println!("Desreferenciado: {}", *referencia); // * explícito
// Múltiples referencias inmutables al mismo tiempo: OK
let s = String::from("texto");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{r1}, {r2}, {r3}"); // Todas válidas simultáneamente
// Comparación: las referencias se desreferencian automáticamente
let a = String::from("hola");
let b = String::from("hola");
let ra = &a;
let rb = &b;
println!("¿Son iguales? {}", ra == rb); // true — compara contenido
}Referencias mutables: &mut T
Una referencia mutable permite modificar el valor prestado:
fn capitalizar(s: &mut String) {
// Podemos modificar el String a través de la referencia mutable
if let Some(primer) = s.get_mut(0..1) {
primer.make_ascii_uppercase();
}
}
fn agregar_puntuacion(s: &mut String, puntuacion: char) {
s.push(puntuacion);
}
fn main() {
let mut mensaje = String::from("hola rust");
capitalizar(&mut mensaje);
println!("{mensaje}"); // "Hola rust"
agregar_puntuacion(&mut mensaje, '!');
println!("{mensaje}"); // "Hola rust!"
}La regla fundamental del borrow checker
El borrow checker de Rust aplica una regla central que previene las condiciones de carrera de datos:
En cualquier momento, puedes tener:
- Una referencia mutable (
&mut T), O - Cualquier número de referencias inmutables (
&T)
Pero nunca ambas al mismo tiempo.
fn main() {
let mut s = String::from("hola");
// Caso 1: Múltiples referencias inmutables — OK
let r1 = &s;
let r2 = &s;
println!("{r1}, {r2}"); // r1 y r2 se usan aquí — sus vidas terminan aquí
// Caso 2: Una referencia mutable — OK (r1 y r2 ya no se usan)
let rm = &mut s;
rm.push_str(" mundo");
println!("{rm}");
// Caso 3 (error): Mutable e inmutable simultáneamente
// let r3 = &s;
// let rm2 = &mut s; // Error: cannot borrow `s` as mutable
// // because it is also borrowed as immutable
// println!("{r3} {rm2}");
}NLL: Non-Lexical Lifetimes
El compilador de Rust es inteligente: las referencias tienen una "vida" que termina en el último punto donde se usan, no necesariamente al final del bloque:
fn main() {
let mut s = String::from("hola");
let r1 = &s;
let r2 = &s;
println!("{r1} y {r2}"); // r1 y r2 terminan aquí (NLL)
// Ahora podemos crear una referencia mutable — r1 y r2 ya no existen
let rm = &mut s;
rm.push_str(" mundo");
println!("{rm}");
}Esto funciona gracias a Non-Lexical Lifetimes (NLL), introducido en Rust 2018. Las referencias no viven hasta el cierre del bloque, sino hasta su último uso.
Referencias colgantes: el borrow checker al rescate
En C/C++, es posible tener un puntero que apunta a memoria que ya fue liberada — un "dangling pointer". Rust hace esto imposible en tiempo de compilación:
// Este código NO compila
fn crear_referencia_colgante() -> &String {
let s = String::from("hola");
&s // Error: s se dropea al salir de esta función
} // s se libera aquí — la referencia apuntaría a memoria inválidaEl compilador rechaza este código con:
error[E0106]: missing lifetime specifier
--> src/main.rs:2:33
|
2 | fn crear_referencia_colgante() -> &String {
| ^ expected named lifetime parameterLa solución es retornar el String directamente (transfiriendo ownership) en lugar de una referencia:
fn crear_string() -> String {
let s = String::from("hola");
s // Transferimos el ownership — no colgante
}Slices: referencias a partes de colecciones
Los slices son un tipo especial de referencia que apuntan a una secuencia contigua de elementos:
fn primera_palabra(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i]; // Slice de la primera palabra
}
}
&s[..] // Si no hay espacio, toda la cadena es una palabra
}
fn main() {
let frase = String::from("hola mundo rust");
// String slices (&str)
let primera = primera_palabra(&frase);
println!("Primera palabra: {primera}");
// Slices de arrays
let numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mitad: &[i32] = &numeros[0..5];
let resto: &[i32] = &numeros[5..];
println!("Mitad: {:?}", mitad);
println!("Resto: {:?}", resto);
// sum(), min(), max() funcionan en slices
let suma: i32 = mitad.iter().sum();
println!("Suma de la primera mitad: {suma}");
}Resumen de las reglas de borrowing
| Tipo de referencia | Permite | Cuántas simultáneas |
|---|---|---|
&T |
Leer | Ilimitadas |
&mut T |
Leer y escribir | Exactamente 1 (y ninguna &T) |
Estas reglas garantizan que:
- No puede haber condiciones de carrera de datos (data races)
- No puede haber punteros colgantes (dangling pointers)
- No puede haber invalidación de iteradores
Todo verificado en tiempo de compilación, sin ningún costo en tiempo de ejecución.
Ahora que entiendes references y borrowing, la siguiente lección profundiza en los lifetimes — las anotaciones que el compilador usa para rastrear cuánto tiempo viven las referencias.
fn calcular_longitud(s: &String) -> usize {
s.len() // Accedemos sin tomar ownership
}
fn agregar_mundo(s: &mut String) {
s.push_str(", mundo");
}
fn main() {
let s1 = String::from("hola");
// & crea una referencia: prestamos s1 sin moverlo
let longitud = calcular_longitud(&s1);
println!("'{s1}' tiene {longitud} caracteres"); // s1 sigue válido
let mut s2 = String::from("hola");
// &mut: referencia mutable — solo UNA a la vez
agregar_mundo(&mut s2);
println!("{s2}"); // "hola, mundo"
// Múltiples referencias inmutables: OK
let r1 = &s1;
let r2 = &s1;
println!("{r1} y {r2}"); // Ambas válidas
// No puedes tener &mut mientras existen &
// let r3 = &s2;
// let r4 = &mut s2; // Error!
}
Inicia sesión para guardar tu progreso