Guía completa de ownership, borrowing y lifetimes en Rust
Dominar ownership, borrowing y lifetimes es la clave para escribir código Rust seguro y eficiente. Esta guía va directo al grano: explicaciones prácticas, ejemplos que fallan y cómo arreglarlos, patrones comunes (Rc/RefCell/Arc) y consejos para depurar errores del compilador.
1. Ownership: regla esencial
Cada valor en Rust tiene un único owner. Cuando el owner sale de scope, el valor se libera. Muchas confusiones vienen de moves y clones.
fn main() {
let s1 = String::from("hola");
let s2 = s1; // s1 se mueve a s2
// println!("{}", s1); // error: uso de valor después de ser movido
let s3 = s2.clone(); // clonamos el contenido, s2 y s3 son independientes
println!("s2={} s3={}", s2, s3);
}
Por qué: mover evita copias implícitas de memoria heap. Clonar es explícito y suele ser más caro.
2. Borrowing: referencias & y &mut
En vez de mover, puedes prestar referencias. Se permite cualquiera cantidad de referencias inmutables simultáneas, pero solo una mutable (y no mezcladas con inmutables).
fn len(s: &String) -> usize { s.len() }
fn push_exclamation(s: &mut String) { s.push('!'); }
fn main() {
let mut s = String::from("hola");
let l = len(&s); // préstamo inmutable
push_exclamation(&mut s); // préstamo mutable
println!("{} (len={})", s, l);
}
Ejemplo de error común: intentar pedir prestado mutable mientras existen préstamos inmutables activos. Refactoriza para limitar el alcance de los préstamos.
3. Lifetimes: evitar referencias colgantes
Los lifetimes son anotaciones que le dicen al compilador cuánto tiempo vive una referencia. Normalmente el compilador infiere los lifetimes, pero a veces debes anotarlos.
Ejemplo que falla (dangling reference)
fn dangle() -> &String {
let s = String::from("hola");
&s // error: devuelve referencia a valor local que será liberado
}
Solución: devolver el propio String, no su referencia.
fn no_dangle() -> String {
let s = String::from("hola");
s
}
Funciones con lifetimes explícitos
Cuando una función devuelve una referencia que depende de parámetros, declara lifetimes:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
Reglas de elisión de lifetimes (cuando no necesitas anotarlas): 1) cada referencia de entrada tiene su propio lifetime; 2) si hay exactamente una referencia &self o &mut self, las salidas la heredan; 3) si hay múltiples referencias de entrada, no hay salida implícita—debes anotar.
4. Patrones prácticos: Rc, Arc, RefCell
Cuando ownership y borrow no bastan (por ejemplo, grafos, estructuras con ciclos, o mutabilidad compartida), usa estas herramientas:
Rc para contabilidad de referencias (single-thread)
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hola"));
let b = Rc::clone(&a); // incrementa contador, barato
println!("count = {}", Rc::strong_count(&a));
println!("a = {} b = {}", a, b);
}
RefCell para mutabilidad interior (checks en runtime)
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut();
v.push(4);
}
println!("{:?}", data.borrow());
// Nota: si pides dos borrow_mut() simultáneos, hará panic en tiempo de ejecución
}
Arc + Mutex para acceso concurrente
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("counter = {}", *counter.lock().unwrap());
}
5. Errores comunes y cómo resolverlos
- Intentar devolver referencia a variable local: devuelve el valor (mueve) o usa smart pointer con mayor alcance.
- Conflicto mutable/inmutable: reduce el scope del préstamo inmutable o usa refactorización para evitar préstamos simultáneos.
- Usar RefCell sin prever panics: verifica borrowing en tiempo de ejecución y documenta las invariantes.
- Clonar por comodidad: revisa el coste; a menudo puedes reorganizar ownership para evitar copias.
6. Debugging: sacar partido al compilador
Los errores del borrow checker parecen crípticos al principio, pero son precisos. Tips:
- Lee el mensaje completo: te indica la ubicación donde el compilador cree que hay conflicto.
- Reduce el código al mínimo reproducible para entender el préstamo que falla.
- Usa
rustc --explain E0391(o el código que te de el compilador) para explicaciones detalladas. - Herramientas:
cargo clippy,miri(undefined behavior checks), yrust-analyzerpara IDE hints.
7. Rendimiento y cuándo clonar
Evita clonar estructuras grandes en bucles; prefiere referencias o reusar buffers. Clonar es la opción segura pero costosa; mide con perfiles antes de optimizar.
8. Ejemplo real: Struct con referencia y solución
struct Owner<'a> {
name: &'a str,
}
fn main() {
let owner;
{
let s = String::from("Alice");
// owner = Owner { name: &s }; // error: s no vive lo suficiente
}
// Solución: hacer que Owner posea el String o usar String en lugar de &str
}
// Correcto:
struct Owner2 {
name: String,
}
fn main_ok() {
let s = String::from("Alice");
let owner = Owner2 { name: s }; // move: owner posee el String
}
8. Recursos y siguientes pasos
Practica con pequeños retos: implementa un lista enlazada segura, un cache con Rc/RefCell o un servidor multithread usando Arc/Mutex. Lee el capítulo de ownership en "The Rust Programming Language" (el libro oficial).
Consejo avanzado: evita la tentación de marcar todo con 'static o usar Arc indiscriminadamente. Aprender a reestructurar el ownership y reducir el alcance de los préstamos suele dar mejores resultados que rodear los problemas con smart pointers.
Advertencia: RefCell y Mutex cambian la seguridad de compilación por checks en runtime; úsalos con cuidado y documenta las invariantes. Siguiente paso: explora cómo las lifetimes interactúan con async/await y aprende sobre GATs (Generic Associated Types) si trabajas con referencias en futures.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación