En esta página

References y borrowing: usar sin poseer

15 min lectura TextoCap. 2 — Ownership y borrowing

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álida

El 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 parameter

La 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.

Las referencias no transfieren ownership
Una referencia &T permite leer el valor sin convertirte en su owner. Cuando la referencia sale de scope, el valor original NO se dropea — solo se descarta la referencia. El owner original conserva la propiedad del dato.
Una sola referencia mutable a la vez
La regla fundamental: puedes tener muchas referencias inmutables (&T) O exactamente una referencia mutable (&mut T), pero nunca ambas al mismo tiempo. Esta regla previene condiciones de carrera en tiempo de compilación.
rust
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!
}