Guía completa de Ownership, Borrowing y Lifetimes en Rust
Dominar ownership, borrowing y lifetimes es clave para escribir código seguro y eficiente en Rust. Esta guía explica los conceptos, muestra ejemplos prácticos, estructura de proyecto y soluciones a errores comunes. Incluye por qué cada patrón existe y cuándo aplicarlo.
1. Conceptos esenciales (rápido)
- Ownership: cada valor en Rust tiene un único propietario. Cuando el propietario sale del scope, el valor se libera.
- Move: transferir ownership de una variable a otra (por defecto para tipos no Copy).
- Borrowing: prestar una referencia (&T o &mut T) sin transferir ownership.
- Lifetimes: anotaciones que describen cuánto viven las referencias para evitar referencias colgantes.
- Interior mutability: patrones como RefCell o Mutex permiten mutar a través de referencias compartidas con comprobación en tiempo de ejecución.
2. Ejemplos básicos
Move y copia
fn main() {
let s1 = String::from("hola");
let s2 = s1; // s1 fue movido a s2
// println!("{}", s1); // ERROR: use of moved value
let x = 5; // i32 es Copy
let y = x; // copia, ambos válidos
println!("{} {}", x, y);
}
Borrowing (inmutable y mutable)
fn main() {
let mut s = String::from("hola");
let r1 = &s; // préstamo inmutable
let r2 = &s; // otro préstamo inmutable
println!("{} {}", r1, r2);
let r3 = &mut s; // ERROR si r1/r2 aún se usan
// Para que funcione, r1 y r2 ya no deben usarse
}
3. Lifetimes: cuándo y cómo anotarlas
Las anotaciones de lifetime raramente son necesarias; el compilador las infiere. Son necesarias cuando las relaciones entre referencias no son obvias.
Función que devuelve la referencia más larga
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let a = String::from("abc");
let b = "xyz";
let l = longest(&a, b);
println!("La más larga: {}", l);
}
Struct con lifetime
struct Ex<'a> {
part: &'a str,
}
impl<'a> Ex<'a> {
fn new(p: &'a str) -> Self { Ex { part: p } }
}
fn main() {
let s = String::from("hola mundo");
let e = Ex::new(&s);
println!("{}", e.part);
}
4. Patrones de propiedad compartida e interior mutability
Para datos compartidos en un solo hilo: Rc y RefCell
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
*a.borrow_mut() += 1; // comprobación en tiempo de ejecución
println!("valor: {}", b.borrow());
}
Para datos compartidos entre hilos: Arc y Mutex
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let d = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut v = d.lock().unwrap();
v.push(4);
});
handle.join().unwrap();
println!("{:?}", data.lock().unwrap());
}
5. Lifetimes y async: advertencia práctica
No puedes devolver referencias que apunten a datos locales desde un async fn; futures deben ser 'static a menos que captures referencias externas con scopes controlados. En la práctica, usa Arc/Box o mueve ownership dentro del futuro:
// Evita:
// async fn bad<'a>(s: &'a str) -> &'a str { ... } // no permitido
// Usa owned or Arc:
use std::sync::Arc;
async fn good(s: Arc) -> usize { s.len() }
6. Errores comunes y cómo solucionarlos
- borrowed value does not live long enough: asegura que el valor referenciado tenga un lifetime al menos igual al de la referencia. Mueve el valor a un scope más alto o cambia a owned/Arc.
- cannot borrow `x` as mutable because it is also borrowed as immutable: ajusta scopes para que las referencias inmutables no se usen cuando necesites una mutable; reduce el alcance o clona si el coste es aceptable.
- use of moved value: recuerda que algunos tipos se mueven. Si necesitas copia semántica, implementa/usa Clone o Copy cuando tenga sentido.
- Send/Sync errors in threads: verifica que los tipos compartidos implementen Send/Sync; usa Arc/Mutex y evita RefCell en contextos multihilo.
7. Buenas prácticas
- Prefiere referencias (&T, &mut T) en APIs cuando el callee no necesita ownership.
- Usa tipos owned (String, Vec) para datos que deben vivir más allá del scope actual o cruzar hilos.
- Aplicar interior mutability solo cuando sea necesario; prefiere mutabilidad clásica si el diseño lo permite.
- Añade bounds Send/Sync en APIs que puedan ejecutarse en hilos.
- Usa Clippy y MIRI para detectar problemas sutiles y patrones anti-idiomáticos.
8. Mini proyecto de ejemplo: ownership-demo
Estructura propuesta:
ownership-demo/
├─ Cargo.toml
└─ src/
├─ main.rs
└─ lib.rs
Cargo.toml (mínimo)
[package]
name = "ownership-demo"
version = "0.1.0"
edition = "2021"
src/lib.rs: funciones reutilizables
pub fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
pub struct Holder<'a> {
pub value: &'a str,
}
impl<'a> Holder<'a> {
pub fn new(v: &'a str) -> Self { Holder { value: v } }
}
src/main.rs: demostración
use ownership_demo::{longest, Holder};
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let a = String::from("hola");
let b = String::from("mundo largo");
println!("Longest: {}", longest(&a, &b));
let h = Holder::new(&b);
println!("Holder: {}", h.value);
let data = Arc::new(Mutex::new(0));
let d = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut num = d.lock().unwrap();
*num += 1;
});
handle.join().unwrap();
println!("count: {}", data.lock().unwrap());
}
Por qué esta estructura: lib.rs contiene la lógica reutilizable y ejemplos de lifetimes. main.rs muestra uso práctico y ejemplos de concurrencia segura con Arc
9. Herramientas y lectura recomendada
- Libro: The Rust Programming Language (enfócate en capítulos de ownership y lifetimes)
- Rustonomicon: para internals y patrones avanzados
- Clippy y Rustfmt: calidad de código
- MIRI: detectar errores indefinidos en tiempo de ejecución
10. Últimos consejos prácticos
Cuando el compilador te dé errores de lifetime, léelos con calma: a menudo indican que la intención de ownership del diseño no coincide con la forma en que pasas datos. Piensa si tu API debería aceptar referencias o valores owned; a veces cambiar a owned (o a Arc) simplifica el uso aunque implique coste de copia/alloc.
Consejo avanzado: para APIs asíncronas que necesitan compartir estado, prefiere tipos sincronizables y owned (Arc<Mutex<T>> o async-aware locks) y evita referencias con lifetime en la firma de async fn. Si necesitas rendimiento extremo y control, estudia Pin/UnsafeCell y Rustonomicon con mucho cuidado.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación