Guía completa de ownership y lifetimes en Rust para desarrolladores
Esta guía te lleva desde los conceptos básicos hasta patrones prácticos para resolver errores comunes del compilador relacionados con ownership, borrowing y lifetimes. Código claro, ejemplos reales y alternativas (Rc/Arc, RefCell, Cow) para cuando las reglas del compilador te limiten.
1. Conceptos clave (rápido)
- Ownership: cada valor en Rust tiene un único propietario. Al moverse el valor, el propietario anterior ya no puede usarlo.
- Borrowing: puedes prestar referencias (&T inmutable, &mut T mutable). Mientras hay una &mut, no pueden existir & simultáneas.
- Lifetimes: anotaciones que garantizan que las referencias sean válidas el tiempo suficiente. El compilador infiere muchas, pero a veces necesitarás especificarlas.
2. Ejemplos básicos
Movimiento y clonación:
fn main() {
let s1 = String::from("hola");
let s2 = s1; // s1 se movió a s2
// println!("{}", s1); // error: uso de valor movido
let s3 = s2.clone(); // copia heap
println!("{} {}", s2, s3);
}
Borrowing inmutable vs mutable:
fn modify_string(s: &mut String) {
s.push_str(" mundo");
}
fn main() {
let mut s = String::from("hola");
modify_string(&mut s);
println!("{}", s);
}
3. Lifetimes: cuándo y por qué
Rust necesita saber que una referencia que devuelves desde una función sigue siendo válida fuera de ella. Ejemplo que falla:
fn dangling() -> &String {
let s = String::from("hola");
&s // error: s no vive lo suficiente
}
No puedes devolver una referencia a algo local. Solución: devolver el valor o usar una referencia con mayor alcance.
4. Anotaciones de lifetime simples
Función que recibe dos referencias y devuelve la con mayor duración (concepto):
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("abcd");
let s2 = "xyz";
let r = longest(s1.as_str(), s2);
println!("Longest: {}", r);
}
Explicación: 'a marca un lifetime común; ambas referencias entrantes y la salida comparten la misma región de validez.
5. Reglas de elisión de lifetimes
Rust aplica reglas para no requerir anotaciones en situaciones comunes (por ejemplo, métodos &self implican que el retorno comparte el lifetime de &self). Añade anotaciones cuando hay múltiples referencias de entrada y la salida no es obvia.
6. Structs con referencias
Si una struct contiene referencias, debes declarar lifetimes:
struct Line<'a> {
start: &'a str,
end: &'a str,
}
fn main() {
let s = String::from("hola mundo");
let a = &s[0..4];
let b = &s[5..];
let line = Line { start: a, end: b };
println!("{} - {}", line.start, line.end);
}
El lifetime 'a asegura que la struct no pueda vivir más que la data referenciada.
7. Patrones útiles y alternativas
- Devolver String en vez de &str: si construir valores conlleva menos complejidad de lifetimes, devuelve proprietario.
- Rc / Arc: para compartir propiedad en tiempo de ejecución (Rc para single-thread, Arc para multithread).
- RefCell / Mutex: para mutabilidad interior con comprobación en runtime.
- Cow: copy-on-write para aceptar &str o String sin clones innecesarios.
use std::rc::Rc;
fn rc_example() {
let s = Rc::new(String::from("compartido"));
let a = Rc::clone(&s);
let b = Rc::clone(&s);
println!("{} {} {}", s, a, b);
}
use std::cell::RefCell;
fn refcell_example() {
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut();
v.push(4);
}
println!("{:?}", data.borrow());
}
8. Errores típicos del compilador y cómo resolverlos
- error[E0382]: use of moved value
Solución: clonar si necesitas dos propietarios, pasar por referencia si no necesitas ownership.
- error[E0505]: cannot move out of borrowed content
Evita mover desde un &T; en su lugar clona o reestructura el código para tomar propiedad antes del préstamo.
- error[E0499]: cannot borrow `x` as mutable because it is also borrowed as immutable
Reorganiza para que los préstamos no se solapen o usa tipos de interior mutability si es necesario.
- error[E0106]: missing lifetime specifier
Añade lifetimes a firmas de funciones/structs cuando devuelvas referencias basadas en múltiples entradas.
9. Patrón: API que acepta tanto &str como String
Usa &&str, generics o Cow para flexibilidad sin clones innecesarios:
use std::borrow::Cow;
fn greet(name: impl Into>) {
let name: Cow<'static, str> = name.into();
println!("Hola, {}!", name);
}
fn main() {
greet("mundo");
greet(String::from("Rust"));
}
10. Buenas prácticas
- Prefiere devolver valores owned si la semántica lo permite — a veces es más simple que lidiar con lifetimes.
- Comienza con referencias y sólo eleva a Rc/RefCell cuando el diseño lo requiera.
- Usa nombres de lifetime descriptivos cuando no sea obvio (por ejemplo, 'input, 'output).
- Evita ciclos de Rc; si aparecen, piensa en Weak para romperlos.
11. Estructura mínima de ejemplo (archivo)
ownership-lifetimes/
├─ src/
│ ├─ main.rs // ejemplos de move/borrow
│ ├─ lib.rs // funciones genéricas con lifetimes
│ └─ utils.rs // Rc/RefCell/Cow ejemplos
└─ Cargo.toml
Ejecuta cargo run para probar los fragmentos y cargo test para cubrir casos de borrow/move.
Avanza practicando: reproduce errores del compilador y corrígelos cambiando ownership, introduciendo clones controlados o usando tipos de interior mutability. Consejo avanzado: cuando trabajes con APIs FFI o multithread, documenta lifetimes y sincronización explícitamente para evitar UB y condiciones de carrera.
Advertencia: abusar de Rc/RefCell/unsafe para evitar lifetimes suele llevar a complejidad y errores a futuro — considera el diseño en raíz antes de romper las reglas del compilador.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación