Move semantics en Rust: por qué let s2 = s1 invalida s1

Cuando escribes let s2 = s1 en Rust con un String, no obtienes dos copias del mismo dato. Obtienes un move: la propiedad del valor se transfiere a s2 y s1 queda invalidado. Esta es la move semantics de Rust, y entenderla es fundamental para trabajar con el lenguaje sin pelear contra el compilador.

Qué hay en la memoria cuando creas un String

Un String en Rust tiene tres campos en el stack:

  • un puntero al dato en el heap
  • la longitud actual
  • la capacidad total reservada
let s1 = String::from("hola");
// Stack: { ptr ? heap, len: 4, capacity: 4 }
// Heap:  h o l a

La asignación mueve, no copia

let s2 = s1;
// Stack s2: { ptr ? mismo heap, len: 4, capacity: 4 }
// s1 queda INVALIDADO

Rust copia los tres campos del stack de s1 a s2, pero no duplica el heap. Además invalida s1 para que nunca haya dos propietarios del mismo dato. Si ambos intentaran liberar la memoria al salir del scope, sería un double free, uno de los bugs más peligrosos en C.

fn main() {
    let s1 = String::from("hola");
    let s2 = s1;

    println!("{}", s1); // ERROR
    println!("{}", s2); // OK
}
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
2 |     let s1 = String::from("hola");
  |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move

Tipos Copy: los que sí se copian

No todos los tipos se mueven. Los tipos que implementan el trait Copy se copian de forma implícita en lugar de moverse:

let x: i32 = 5;
let y = x; // copia, no move

println!("{}", x); // OK, x sigue válido
println!("{}", y); // OK

Los tipos Copy son los que viven completamente en el stack y tienen tamaño conocido en tiempo de compilación:

  • Enteros: i8, i16, i32, i64, i128, isize, u8…
  • Flotantes: f32, f64
  • bool
  • char
  • Tuplas cuyos elementos son todos Copy: (i32, bool)
  • Arrays de tipos Copy: [i32; 4]

String, Vec, Box y cualquier tipo con datos en el heap no son Copy.

Move en llamadas a funciones

fn imprimir(s: String) {
    println!("{}", s);
} // s se destruye aquí

fn main() {
    let msg = String::from("hola");
    imprimir(msg); // move: msg ya no es válido
    // println!("{}", msg); // ERROR
}

Pasar un valor a una función es semánticamente idéntico a asignarlo. Si quieres conservar la propiedad, tienes dos opciones: devolver el valor desde la función o usar borrowing (referencias).

Clone: cuando necesitas una copia real

Si realmente necesitas dos copias independientes del dato en el heap, usa .clone():

let s1 = String::from("hola");
let s2 = s1.clone(); // duplica el heap

println!("{}", s1); // OK
println!("{}", s2); // OK

.clone() es una operación explícita y potencialmente cara. Rust te hace escribirla a mano para que seas consciente de que estás duplicando datos.

Move en enums y structs

struct Punto {
    x: f64,
    y: f64,
}

struct Mensaje {
    contenido: String,
}

fn main() {
    let p1 = Punto { x: 1.0, y: 2.0 };
    let p2 = p1; // Punto es Copy si sus campos son Copy… pero no lo es por defecto
    // Necesitarías #[derive(Copy, Clone)] para que funcione sin move

    let m1 = Mensaje { contenido: String::from("hola") };
    let m2 = m1; // move: m1 queda invalidado
}

Resumen

  • La asignación en Rust mueve los valores que no son Copy.
  • El original queda invalidado: no hay double free ni aliasing accidental.
  • Los tipos simples del stack (i32, bool, char…) implementan Copy y se copian automáticamente.
  • Para duplicar un tipo no Copy, usa .clone() de forma explícita.

Una vez que entiendes que la asignación mueve, el siguiente paso es borrowing: cómo pasar referencias a datos sin transferir la propiedad, que es el mecanismo que usarás en la gran mayoría del código Rust real.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP