Rust: Ownership

Descubre el sistema de ownership en Rust y cómo garantiza seguridad y eficiencia en la gestión de memoria sin recolector de basura.

Aprende Rust GRATIS y certifícate

Ownership en Rust

El ownership (propiedad) es el concepto fundamental que distingue a Rust de otros lenguajes de programación. Este sistema permite que Rust garantice la seguridad de memoria sin necesidad de un recolector de basura, eliminando errores comunes como los accesos a memoria liberada o las fugas de memoria.

El sistema de propiedad

En Rust, cada valor tiene un único propietario (owner) en cualquier momento dado. Cuando el propietario sale de su ámbito de ejecución, el valor se libera automáticamente. Este mecanismo resuelve uno de los problemas más complejos en programación de sistemas: la gestión manual de memoria.

fn main() {
    let s = String::from("hola"); // s es el propietario de la cadena
    println!("{}", s);
} // s sale del ámbito, la memoria se libera automáticamente

El compilador de Rust verifica estas reglas en tiempo de compilación, lo que significa que los errores de memoria se detectan antes de que el programa se ejecute.

Reglas fundamentales del ownership

El sistema se basa en tres reglas inmutables:

  • Cada valor tiene exactamente un propietario
  • Solo puede existir un propietario a la vez
  • Cuando el propietario sale del ámbito, el valor se destruye

Estas reglas se aplican tanto a tipos primitivos como a tipos más complejos, aunque con comportamientos diferentes según si implementan el trait Copy.

fn main() {
    let x = 5;        // x posee el valor 5
    let y = x;        // se copia el valor (i32 implementa Copy)
    
    let s1 = String::from("mundo");
    let s2 = s1;      // s1 transfiere la propiedad a s2
    // println!("{}", s1); // Error: s1 ya no es válido
}

Transferencia de propiedad (move)

Cuando asignamos una variable que contiene datos en el heap a otra variable, ocurre una transferencia de propiedad o move. La variable original deja de ser válida para evitar la doble liberación de memoria.

fn tomar_propiedad(cadena: String) {
    println!("Recibí: {}", cadena);
} // cadena se destruye aquí

fn main() {
    let mi_string = String::from("ejemplo");
    tomar_propiedad(mi_string);
    // mi_string ya no es válido después de la llamada
}

Este comportamiento es diferente al de lenguajes como C++ o Java, donde las asignaciones típicamente crean copias superficiales que pueden llevar a problemas de memoria.

Devolución de propiedad

Las funciones pueden devolver la propiedad de los valores, permitiendo que el código llamador recupere el control sobre los datos:

fn crear_string() -> String {
    let s = String::from("creada en función");
    s // devuelve la propiedad de s
}

fn procesar_y_devolver(mut s: String) -> String {
    s.push_str(" procesada");
    s // devuelve la propiedad modificada
}

fn main() {
    let cadena1 = crear_string();
    let cadena2 = procesar_y_devolver(cadena1);
    println!("{}", cadena2);
}

Clonación explícita

Cuando necesitamos crear una copia profunda de datos en el heap, utilizamos el método clone(). Esta operación es explícita y potencialmente costosa:

fn main() {
    let s1 = String::from("original");
    let s2 = s1.clone(); // copia profunda explícita
    
    println!("s1: {}, s2: {}", s1, s2); // ambas son válidas
}

La clonación debe usarse conscientemente, ya que implica asignación de memoria adicional y copia de datos.

Tipos que implementan Copy

Los tipos primitivos como enteros, flotantes y booleanos implementan el trait Copy, lo que significa que se copian automáticamente en lugar de moverse:

fn main() {
    let x = 42;
    let y = x;           // x se copia, no se mueve
    println!("{} {}", x, y); // ambos son válidos
    
    let tupla = (5, true);
    let otra_tupla = tupla;  // se copia porque todos sus elementos implementan Copy
    println!("{:?} {:?}", tupla, otra_tupla);
}

Los tipos que contienen datos en el heap, como String o Vec<T>, no pueden implementar Copy porque requerirían asignación de memoria para cada copia.

Ownership en estructuras de datos

Las estructuras personalizadas siguen las mismas reglas de ownership. Cuando una estructura contiene tipos que no implementan Copy, la estructura completa se mueve:

struct Persona {
    nombre: String,
    edad: u32,
}

fn main() {
    let persona1 = Persona {
        nombre: String::from("Ana"),
        edad: 30,
    };
    
    let persona2 = persona1; // persona1 se mueve a persona2
    // println!("{}", persona1.nombre); // Error: persona1 ya no es válida
    println!("{}", persona2.nombre);   // Válido
}

El sistema de ownership en Rust transforma la gestión de memoria de una responsabilidad manual propensa a errores en un conjunto de reglas verificadas automáticamente por el compilador, proporcionando tanto seguridad como rendimiento.

Empezar curso de Rust

Lecciones de este módulo de Rust

Lecciones de programación del módulo Ownership del curso de Rust.

Ejercicios de programación en este módulo de Rust

Evalúa tus conocimientos en Ownership con ejercicios de programación Ownership de tipo Test, Puzzle, Código y Proyecto con VSCode.