En Rust, el modelo de propiedad (ownership) dicta que cada valor tiene un único dueño. Cuando asignas una variable a otra (let b = a), el comportamiento de esa operación depende de si el tipo implementa el trait Copy o si se realiza un “move” (movimiento).

El “move semantics” es la base del sistema de memoria de Rust. Si un tipo posee recursos en el heap (como String), Rust no duplica la información por defecto al realizar una asignación; en su lugar, transfiere la propiedad. Esto evita la duplicación de punteros que apuntan a la misma dirección de memoria, lo que previene el error de “double free” (intentar liberar la misma memoria dos veces) al llamar al destructor Drop. Si intentas usar una variable después de que su valor haya sido movido, el compilador lanzará el error use of moved value.

Existen dos formas de duplicar datos:
1. Copy (Copia implícita): Se aplica a tipos que residen puramente en el stack (como enteros i32, booleanos bool o tuplas de tipos Copy). Es una copia bit a bit rápida y económica. Al asignar let b = a, ambos son válidos y viven en el stack de forma independiente.
2. Clone (Copia explícita): Se utiliza para tipos que poseen datos en el heap. A diferencia de Copy, clone() es una operación explícita porque puede ser costosa (requiere alocar nueva memoria y copiar el contenido del heap). Por ejemplo, s2 = s1.clone() crea una réplica exacta de la cadena, permitiendo que tanto s1 como s2 sean dueños de sus propios datos.

A diferencia de C++, donde std::move es un cast que marca un objeto para ser movido, en Rust el movimiento es la acción por defecto para tipos no Copy.

fn main() {
    // --- Caso 1: Tipos Copy (Escalares) ---
    let n1 = 100;
    let n2 = n1; // Se realiza una copia de bits en el stack
    println!("n1: {}, n2: {}", n1, n2); // Ambos son válidos

    // --- Caso 2: Move Semantics (String) ---
    let s1 = String::from("Rust");
    let s2 = s1; // El valor se mueve de s1 a s2. s1 queda invalidado.
    
    // println!("{}", s1); // ERROR: use of moved value: `s1`
    println!("s2: {}", s2); // s2 es el nuevo dueño

    // --- Caso 3: Clone (Copia profunda en el Heap) ---
    let s3 = s2.clone(); // Se aloca nueva memoria en el heap para s3
    println!("s2: {}, s3: {}", s2, s3); // Ambos son válidos y son independientes

    // --- Caso 4: Mover valores en funciones ---
    let s4 = String::from("Propiedad");
    take_ownership(s4); 
    // println!("{}", s4); // ERROR: s4 fue movido dentro de la función
}

fn take_ownership(val: String) {
    println!("Consumiendo: {}", val);
} // Aquí 'val' sale de scope y su memoria se libera (Drop)

Análisis del código

  • Caso 1 (n1, n2): n1 es de tipo i32. Como los enteros implementan el trait Copy, la asignación let n2 = n1 no invalida a n1. Ambos mantienen sus valores en el stack.
  • Caso 2 (s1, s2): s1 es un String (almacena un puntero al heap). Al hacer let s2 = s1, la propiedad del puntero se transfiere a s2. El compilador invalida s1 para evitar que, al terminar la función, ambos intenten liberar el mismo puntero de String.
  • Caso 3 (s3): Para evitar la invalidez de s2, llamamos a .clone(). Esto realiza una copia profunda en el heap. s2 mantiene su dirección de memoria original y s3 obtiene una nueva dirección con el mismo contenido.
  • Caso 4 (s4, val): Al pasar s4 como argumento a la función take_ownership, ocurre un “move”. La variable val en la función se convierte en la nueva dueña del String. Cuando la función termina, val sale de scope y se ejecuta su Drop, liberando la memoria. Por lo tanto, s4 ya no es válido en el main.

25