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.
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.
- Elegir entre
&self,&mut selfyselfsegú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 selfpresta el struct entero, no un campo. - Distinguir
iter,iter_muteinto_itercomo 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}");
}
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.
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.
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.
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.
- Define un
struct Cuenta { titular: String, saldo: i64 }contitular(&self) -> &str,ingresar(&mut self, n: i64)ycerrar(self) -> i64. - Presta a la vez
&cuenta.titulary&mut cuenta.saldoy comprueba que compila por ser campos disjuntos. - Provoca un choque llamando a un método
&selfy guardando su referencia, y luego a un método&mut self; lee el E0502 y resuélvelo accediendo a los campos directamente. - Recorre un
Vec<i32>con&v, con&mut v(multiplicando por diez) y convpor valor; anota en cuál se consume el contenedor. - Para cada método de tu
Cuenta, justifica por qué elegiste ese receptor y no uno más permisivo.