Guía completa de Ownership, Borrowing y Lifetimes en Rust

rust Guía completa de Ownership, Borrowing y Lifetimes en Rust

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.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión