wandres.dev
BORROWING · referencias y reglas

Borrowing en la práctica: &self, &mut self y préstamos parciales

Cómo se manifiesta el borrowing en el día a día: los receptores `&self` y `&mut self` en métodos, los préstamos parciales de campos disjuntos de un struct, y por qué un método presta el struct entero.

⏱ 17 min

Las reglas del préstamo dejan de ser abstractas en cuanto diseñas tipos. Cada método declara qué acceso necesita —&self para mirar, &mut self para cambiar, self para consumir—, y esa elección se convierte en el contrato de tu API. Aquí reunimos el borrowing con lo que ya sabes de impl y structs: receptores, préstamos parciales de campos disjuntos, el límite del préstamo por método, y los patrones de iteración que usarás a diario.

🎯 Al terminar esta lección sabrás
  • Elegir entre &self, &mut self y self según el acceso que necesite el método.
  • Aprovechar los préstamos parciales: prestar campos disjuntos a la vez.
  • Entender por qué un método &mut self presta el struct entero, no un campo.
  • Distinguir iter, iter_mut e into_iter como los tres modos de recorrer.

Tres receptores, tres contratos

El receptor de un método es una decisión de ownership grabada en el tipo. &self toma prestado el valor de forma compartida (solo lee); &mut self lo toma en exclusiva (puede mutar); self se apropia de él (lo consume o lo transforma).

struct Contador { valor: u32 }

impl Contador {
    fn valor(&self) -> u32 { self.valor }          // solo lee: el llamador conserva el Contador
    fn incrementa(&mut self) { self.valor += 1; }  // muta en exclusiva: exige un binding `mut`
    fn consumir(self) -> u32 { self.valor }        // se apropia: tras llamarlo, el Contador ya no existe
}

fn main() {
    let mut c = Contador { valor: 0 };
    c.incrementa();
    c.incrementa();
    println!("{}", c.valor());   // 2: `c` sigue vivo tras leer
    let final_ = c.consumir();   // `c` se consume aquí
    println!("{final_}");        // 2
}

La regla de diseño es pedir el acceso mínimo que baste: prefiere &self, sube a &mut self solo si mutas, y reserva self para transformar o consumir. Cada nivel de más restringe a quien usa tu tipo.

👀

&self · consulta

Presta el struct para leer; el llamador lo conserva. El receptor por defecto de cualquier getter o consulta.

✍️

&mut self · mutación

Presta el struct en exclusiva para cambiarlo. Exige un binding mut. Para setters y operaciones que alteran el estado.

🎁

self · consumo

Se apropia del struct y lo destruye o transforma. Para conversiones y métodos que cierran el valor.

Préstamos parciales: campos disjuntos a la vez

El borrow checker no razona sobre el struct como un bloque opaco: rastrea préstamos campo a campo. Por eso puedes prestar dos campos distintos simultáneamente —uno compartido y otro mutable— sin que choquen, porque son posiciones de memoria disjuntas que nunca se solapan.

struct Documento {
    titulo: String,
    cuerpo: String,
}

fn main() {
    let mut doc = Documento {
        titulo: String::from("Nivel 9"),
        cuerpo: String::from("borrowing"),
    };

    let t = &doc.titulo;        // préstamo COMPARTIDO del campo `titulo`
    let c = &mut doc.cuerpo;    // préstamo MUTABLE de OTRO campo: disjuntos, permitido
    c.push_str(" a fondo");
    println!("{t}: {}", c.len());
}

Esto parecería violar la regla “compartido y mutable no coexisten”, pero no la viola: la regla es por valor, y titulo y cuerpo son valores distintos. Como el acceso es directo a campos concretos, el checker demuestra que no hay aliasing y permite ambos préstamos. Este split borrow es el pan de cada día al escribir métodos que tocan varios campos.

El límite: un método presta el struct entero

El préstamo parcial funciona con acceso directo a campos. En cuanto pasas por un método, se pierde: un método con receptor &mut self presta todo self, aunque por dentro solo toque un campo. El compilador no mira el cuerpo del método al comprobar la llamada, solo su firma, y la firma dice “me prestas el struct completo”.

impl Documento {
    fn titulo(&self) -> &str { &self.titulo }
    fn anexar(&mut self, s: &str) { self.cuerpo.push_str(s); }
}

fn main() {
    let mut doc = Documento {
        titulo: String::from("t"),
        cuerpo: String::from("c"),
    };

    // let t = doc.titulo();   // `&self`: presta TODO `doc`
    // doc.anexar(" x");       // ERROR E0502: `&mut self` choca con el `&` de `t` aún vivo

    // Solución 1: accede a los campos directamente y recupera el préstamo parcial.
    let t = &doc.titulo;
    doc.cuerpo.push_str(" x");   // campos disjuntos: compila
    println!("{t}");
}
⚠️
Cuando dos métodos chocan, piensa en campos disjuntos

El choque “no puedo llamar a este &mut self porque tengo vivo un &self” es de los más frecuentes al empezar. La causa casi nunca es un error lógico: es que dos métodos, cada uno prestando el struct entero, se solapan. Las salidas idiomáticas son tres: acceder a los campos directamente para que el checker vea que son disjuntos; reestructurar para que el préstamo compartido termine antes (recuerda NLL) de llamar al mutable; o extraer una función libre que tome los campos concretos por referencia en vez de tomar &mut self.

flowchart TB
s[Struct con dos campos] --> c1[Campo titulo]
s --> c2[Campo cuerpo]
c1 -->|prestamo compartido| r1[Referencia de lectura]
c2 -->|prestamo mutable| r2[Referencia de escritura]
r1 --> ok[Coexisten porque son campos disjuntos]
r2 --> ok

Los tres modos de recorrer: iter, iter_mut, into_iter

La misma trinidad compartido / mutable / por valor gobierna cómo iteras una colección. Elegir el método correcto es elegir qué acceso quieres a cada elemento y si conservas o consumes el contenedor.

let mut v = vec![1, 2, 3];

for x in &v {           // iter(): presta cada elemento como `&i32`
    print!("{x} ");     // 1 2 3   -- `v` sigue intacto
}

for x in &mut v {       // iter_mut(): presta cada elemento como `&mut i32`
    *x *= 10;           // muta en el sitio a través del préstamo exclusivo
}
println!("{v:?}");      // [10, 20, 30]

for x in v {            // into_iter(): CONSUME `v`, cada `x` es un `i32` por valor
    print!("{x} ");     // 10 20 30 -- tras el bucle, `v` ya no existe
}

for x in &v y for x in &mut v son el azúcar de v.iter() y v.iter_mut(); for x in v invoca into_iter y se apropia del Vec. La misma decisión de siempre —mirar, mutar o consumir— resuelta en la elección de una de estas tres formas.

Esta trinidad no es exclusiva del Vec: HashMap, String, BTreeSet y prácticamente toda colección de la biblioteca estándar exponen iter, iter_mut e into_iter, gobernadas por el mismo trait IntoIterator. Aprender a elegir entre las tres una sola vez te sirve para recorrer cualquier estructura de forma idiomática, prestando cuando solo quieres leer y consumiendo cuando ya no necesitas el contenedor.

💡
El préstamo compartido es tu opción por defecto

Ante la duda, empieza prestando con &. La mayoría del código solo necesita leer, y el préstamo compartido es el acceso más barato, más flexible y el que menos restringe a los demás. Sube a &mut únicamente cuando de verdad tengas que escribir, y mueve por valor solo cuando la semántica sea consumir o transformar. Este ritmo —prestar por defecto, mutar cuando toca, mover cuando cedes— es lo que da a Rust idiomático su equilibrio entre seguridad y rendimiento sin .clone() regados por todas partes.

El borrowing es el idioma en que se escriben los contratos de acceso

Cierra el nivel entendiendo qué has ganado de verdad. El borrowing no es un peaje del borrow checker, es un vocabulario para expresar, en el propio tipo, quién puede tocar qué y durante cuánto. Cuando eliges &self frente a &mut self, cuando aceptas &str en vez de String, cuando repartes préstamos parciales de campos disjuntos, estás escribiendo un contrato que el compilador convierte en garantía: nadie mutará lo que prometiste solo leer, nadie usará lo que ya consumiste, ninguna referencia sobrevivirá a su dato. La consecuencia más profunda es que las firmas de Rust son documentación ejecutable del flujo de datos: leer los receptores y los & de un tipo te dice cómo circula la propiedad y el acceso por tu programa sin ejecutar una línea. El principiante ve el borrow checker como un guardián que le dice “no”; el experto lo ve como un asistente de diseño que le obliga a decidir, en cada frontera, cuál es el mínimo acceso necesario. Y esa disciplina —pedir siempre lo mínimo, prestar en vez de poseer, hacer explícita cada capacidad— no solo produce programas seguros: produce arquitecturas donde las dependencias de datos son visibles, locales y demostradas. Todo lo que viene después en Rust —lifetimes con nombre, smart pointers, concurrencia sin miedo— son extensiones de este mismo idioma. Ya sabes hablarlo.

📝
Lo esencial del borrowing práctico

El receptor de un método declara su contrato: &self para leer, &mut self para mutar en exclusiva, self para consumir; pide siempre el más débil que baste. El borrow checker razona campo a campo, así que puedes prestar campos disjuntos a la vez —uno compartido, otro mutable—; pero un método presta el struct entero. Y al iterar, &v, &mut v y v eligen entre mirar, mutar y consumir.

⚔️ Borrowing del mundo real
  1. Define un struct Cuenta { titular: String, saldo: i64 } con titular(&self) -> &str, ingresar(&mut self, n: i64) y cerrar(self) -> i64.
  2. Presta a la vez &cuenta.titular y &mut cuenta.saldo y comprueba que compila por ser campos disjuntos.
  3. Provoca un choque llamando a un método &self y guardando su referencia, y luego a un método &mut self; lee el E0502 y resuélvelo accediendo a los campos directamente.
  4. Recorre un Vec<i32> con &v, con &mut v (multiplicando por diez) y con v por valor; anota en cuál se consume el contenedor.
  5. Para cada método de tu Cuenta, justifica por qué elegiste ese receptor y no uno más permisivo.