En esta página
Ownership: el corazón de Rust
Ownership: el concepto central de Rust
El sistema de ownership (propiedad) es lo que distingue a Rust de todos los demás lenguajes de programación. Es el mecanismo que permite a Rust garantizar seguridad de memoria sin un recolector de basura (GC).
Para entender por qué ownership existe, primero necesitamos entender el problema que resuelve.
El problema: gestión de memoria
Todos los programas deben gestionar la memoria que usan. Existen dos enfoques principales:
Recolección de basura (GC): El lenguaje rastrea automáticamente qué memoria ya no se usa y la libera. Ventaja: simple para el programador. Desventaja: pausas impredecibles, mayor uso de memoria, no apto para sistemas en tiempo real.
Gestión manual: El programador es responsable de
malloc/freeonew/delete. Ventaja: control total. Desventaja: errores de use-after-free, double-free, memory leaks, buffer overflows.
Rust toma un tercer camino: el compilador verifica reglas de ownership en tiempo de compilación y genera código que libera memoria automáticamente en el momento exacto correcto, sin ningún overhead en tiempo de ejecución.
Las tres reglas de Ownership
Rust tiene exactamente tres reglas:
- Cada valor en Rust tiene exactamente un owner (propietario).
- Solo puede haber un owner a la vez.
- Cuando el owner sale de scope, el valor se descarta (drop).
Estas reglas suenan simples pero tienen implicaciones profundas.
Stack vs Heap
Para entender ownership, necesitas tener claro la diferencia entre stack y heap:
Stack (pila):
- Memoria LIFO (last in, first out)
- Tamaño conocido en tiempo de compilación
- Asignación y desasignación instantáneas
- Muy rápido
- Ejemplos:
i32,f64,bool,char,[i32; 5]
Heap:
- Memoria dinámica, puede crecer o reducirse
- El OS busca un espacio libre y devuelve un puntero
- Más lento que el stack
- Requiere gestión explícita (en C/C++) u ownership (en Rust)
- Ejemplos:
String,Vec<T>,Box<T>
fn main() {
// En el stack: tamaño fijo, copiado automáticamente
let x: i32 = 5; // 4 bytes en el stack
let y = x; // Copia trivial
println!("x={x}, y={y}"); // Ambos válidos
// En el heap: String tiene 3 partes en el stack:
// - puntero al heap (8 bytes)
// - longitud actual (8 bytes)
// - capacidad (8 bytes)
// Más el contenido en el heap
let s1 = String::from("hola");
println!("s1 apunta a {} bytes en el heap", s1.capacity());
}Move semantics (semántica de movimiento)
Cuando asignas un valor de heap a otra variable, Rust realiza un move (movimiento), no una copia:
fn main() {
let s1 = String::from("hola");
let s2 = s1; // s1 se MUEVE a s2
// En este punto:
// - s2 es el owner del String "hola"
// - s1 ya NO es válido — el compilador lo sabe
println!("{s2}"); // OK
// println!("{s1}"); // Error: "value borrowed here after move"
}¿Por qué no simplemente copiar? Si Rust copiara automáticamente, tendríamos dos owners del mismo dato en el heap. Cuando ambos salieran de scope, se intentaría liberar la misma memoria dos veces — un error de "double free" que causa comportamiento indefinido.
En lugar de copiar, Rust invalida s1 cuando se mueve a s2. Solo s2 libera la memoria cuando sale de scope.
Clone: copia explícita del heap
Cuando sí necesitas una copia completa de un valor en el heap, usa .clone():
fn main() {
let s1 = String::from("hola");
let s2 = s1.clone(); // Copia profunda explícita
println!("s1 = {s1}"); // OK: s1 sigue siendo válido
println!("s2 = {s2}"); // OK: s2 es una copia independiente
// Modificar s2 no afecta s1
let mut s3 = s1.clone();
s3.push_str(" mundo");
println!("s1 = {s1}"); // "hola"
println!("s3 = {s3}"); // "hola mundo"
}El punto clave: .clone() es costoso y explícito. Cuando lo ves en el código, sabes que hay una copia de heap ocurriendo.
El trait Copy
Los tipos "baratos de copiar" implementan el trait Copy. Para estos tipos, la asignación siempre crea una copia en lugar de un move:
fn main() {
// Tipos Copy: la asignación copia, no mueve
let a: i32 = 10;
let b = a; // Copia
let c: f64 = 3.14;
let d = c; // Copia
let e: bool = true;
let f = e; // Copia
let g: char = 'Ñ';
let h = g; // Copia
// Todos siguen siendo válidos
println!("{a} {b} {c} {d} {e} {f} {g} {h}");
// Las referencias &T son Copy (la referencia se copia, no el dato)
let s = String::from("texto");
let r1: &String = &s;
let r2 = r1; // Copia la referencia
println!("{r1} {r2}"); // Ambas válidas
}Un tipo puede implementar Copy solo si todos sus campos también son Copy. String no puede ser Copy porque gestiona memoria del heap.
Ownership y funciones
El ownership sigue las mismas reglas cuando pasas valores a funciones:
fn main() {
let s = String::from("Rust");
// Pasar s MUEVE el ownership a la función
imprimir(s);
// println!("{s}"); // Error: s fue movido
// Para no perder el ownership, la función puede retornarlo
let s2 = String::from("Ownership");
let s2 = retornar_ownership(s2);
println!("Recibí de vuelta: {s2}");
// Esto es verboso — las referencias (siguiente lección) lo solucionan
}
fn imprimir(cadena: String) {
println!("{cadena}");
} // cadena se dropea aquí
fn retornar_ownership(cadena: String) -> String {
println!("{cadena}");
cadena // Retorna el ownership al caller
}Drop: liberación automática de memoria
Cuando un owner sale de scope, Rust llama automáticamente al destructor del valor, implementado a través del trait Drop:
struct RecursoImportante {
nombre: String,
}
impl Drop for RecursoImportante {
fn drop(&mut self) {
println!("Liberando recurso: {}", self.nombre);
}
}
fn main() {
let r1 = RecursoImportante { nombre: String::from("Conexión DB") };
let r2 = RecursoImportante { nombre: String::from("Archivo de log") };
println!("Recursos creados");
// Al salir de scope, Rust llama drop() automáticamente
// en orden inverso de creación: r2 primero, luego r1
println!("Terminando main...");
} // r2.drop() luego r1.drop() — sin intervención del programadorEste patrón, llamado RAII (Resource Acquisition Is Initialization), garantiza que los recursos siempre se liberan, incluso si hay una condición de error. No es posible olvidar cerrar una conexión de base de datos en Rust.
Ownership con estructuras de control
fn main() {
let nombre = String::from("Rustáceo");
// El ownership se mueve al bloque if que lo consume
if nombre.len() > 3 {
let saludo = format!("¡Hola, {nombre}!");
println!("{saludo}");
// nombre se puede usar aquí
println!("Tu nombre tiene {} letras", nombre.len());
}
// nombre sigue disponible aquí porque no fue movido
println!("Nombre final: {nombre}");
// Ejemplo donde SÍ se mueve:
let datos = String::from("datos importantes");
let procesados = procesar(datos); // datos se mueve
// datos ya no está disponible
println!("Procesado: {procesados}");
}
fn procesar(entrada: String) -> String {
entrada.to_uppercase()
}El sistema de ownership puede sentirse restrictivo al principio, pero es el fundamento de todas las garantías de seguridad de Rust. La siguiente lección te enseñará references y borrowing — la forma elegante de usar datos sin transferir ownership.
fn main() {
// String en el heap — tiene owner
let s1 = String::from("hola");
// MOVE: s1 se mueve a s2, s1 ya no es válido
let s2 = s1;
// println!("{s1}"); // Error: value moved
println!("s2 = {s2}");
// CLONE: copia profunda del heap
let s3 = s2.clone();
println!("s2 = {s2}, s3 = {s3}");
// Tipos Copy viven en el stack: se copian automáticamente
let x: i32 = 5;
let y = x; // Copia, no move
println!("x = {x}, y = {y}"); // Ambos válidos
// Ownership y funciones
let cadena = String::from("mundo");
tomar_ownership(cadena);
// println!("{cadena}"); // Error: moved
let numero = 42_i32;
hacer_copia(numero);
println!("numero sigue siendo {numero}"); // Válido: i32 es Copy
}
fn tomar_ownership(s: String) {
println!("Tomé ownership de: {s}");
} // s se dropea aquí
fn hacer_copia(n: i32) {
println!("Tengo una copia: {n}");
}
Inicia sesión para guardar tu progreso