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):n1es de tipoi32. Como los enteros implementan el traitCopy, la asignaciónlet n2 = n1no invalida an1. Ambos mantienen sus valores en el stack. - Caso 2 (
s1,s2):s1es unString(almacena un puntero al heap). Al hacerlet s2 = s1, la propiedad del puntero se transfiere as2. El compilador invalidas1para evitar que, al terminar la función, ambos intenten liberar el mismo puntero deString. - Caso 3 (
s3): Para evitar la invalidez des2, llamamos a.clone(). Esto realiza una copia profunda en el heap.s2mantiene su dirección de memoria original ys3obtiene una nueva dirección con el mismo contenido. - Caso 4 (
s4,val): Al pasars4como argumento a la funcióntake_ownership, ocurre un “move”. La variablevalen la función se convierte en la nueva dueña delString. Cuando la función termina,valsale de scope y se ejecuta suDrop, liberando la memoria. Por lo tanto,s4ya no es válido en elmain.
N° 25