Ownership: El Corazón de Rust
El sistema de ownership (propiedad) es la característica más distintiva de Rust y lo que permite garantizar seguridad de memoria sin un recolector de basura. Entender ownership es fundamental para programar efectivamente en Rust.
¿Qué es Ownership?
Ownership es un sistema que gestiona la memoria automáticamente siguiendo un conjunto de reglas que el compilador verifica en tiempo de compilación. Si cualquiera de estas reglas se viola, el programa no compilará.
Las Tres Reglas de Ownership
- Cada valor en Rust tiene un único propietario
- Solo puede haber un propietario a la vez
- Cuando el propietario sale del scope, el valor se elimina
Scope (Ámbito)
Un scope es el rango dentro del programa donde una variable es válida:
fn main() {
// s no está disponible aquí, no ha sido declarada aún
{
let s = "hola"; // s es válida desde este punto
// hacer cosas con s
println!("{s}");
} // este scope termina, s ya no es válida
// println!("{s}"); // ERROR: s no está en scope
}
Tipos de Datos y Memoria
Stack vs Heap
fn main() {
// Datos en el stack (tamaño conocido, fijo)
let x = 5; // i32 en stack
let y = x; // se copia el valor
println!("x: {x}, y: {y}"); // Ambos son válidos
// Datos en el heap (tamaño variable)
let s1 = String::from("hola"); // String en heap
let s2 = s1; // se mueve la propiedad
// println!("{s1}"); // ERROR: s1 ya no es válido
println!("{s2}"); // Solo s2 es válido
}
Tipos Copy vs Move
fn main() {
// Tipos que implementan Copy (se duplican automáticamente)
let x = 5;
let y = x; // x se copia
println!("x: {x}, y: {y}"); // Ambos funcionan
// Tipos que no implementan Copy (se mueven)
let s1 = String::from("mundo");
let s2 = s1; // s1 se mueve a s2
// println!("{s1}"); // ERROR: value borrowed here after move
// Para duplicar, usa clone()
let s3 = String::from("rust");
let s4 = s3.clone(); // Copia profunda
println!("s3: {s3}, s4: {s4}"); // Ambos funcionan
}
Tipos que Implementan Copy
fn main() {
// Tipos escalares
let a = 5; // i32
let b = true; // bool
let c = 'R'; // char
let d = 3.14; // f64
// Tuplas de tipos Copy
let punto = (3, 4); // (i32, i32)
let otro_punto = punto; // Se copia
println!("Original: {:?}", punto);
println!("Copia: {:?}", otro_punto);
// Arrays de tipos Copy
let array = [1, 2, 3, 4, 5];
let otro_array = array; // Se copia
println!("Original: {:?}", array);
println!("Copia: {:?}", otro_array);
}
Ownership y Funciones
Pasar Valores a Funciones
fn main() {
let s = String::from("hola"); // s entra en scope
tomar_ownership(s); // s se mueve a la función
// s ya no es válido aquí
// println!("{s}"); // ERROR: value borrowed here after move
let x = 5; // x entra en scope
hacer_copia(x); // x se copia a la función
// x sigue siendo válido
println!("x sigue siendo: {x}"); // Esto funciona
} // x sale de scope, pero s ya había salido cuando se movió
fn tomar_ownership(texto: String) { // texto entra en scope
println!("{texto}");
} // texto sale de scope y se elimina
fn hacer_copia(entero: i32) { // entero entra en scope
println!("{entero}");
} // entero sale de scope, pero no pasa nada especial
Retornar Valores desde Funciones
fn main() {
let s1 = dar_ownership(); // función mueve su valor de retorno a s1
let s2 = String::from("hola"); // s2 entra en scope
let s3 = tomar_y_dar_ownership(s2); // s2 se mueve a la función,
// que mueve su valor de retorno a s3
println!("s1: {s1}");
// println!("{s2}"); // ERROR: s2 ya no es válido
println!("s3: {s3}");
} // s3 sale de scope y se elimina
// s1 sale de scope y se elimina
// s2 ya había salido de scope, no pasa nada
fn dar_ownership() -> String { // función mueve su valor de retorno
let texto = String::from("tuyo"); // texto entra en scope
texto // texto se retorna y sale de scope
}
fn tomar_y_dar_ownership(texto: String) -> String { // texto entra en scope
texto // texto se retorna y sale de scope
}
Patrones Comunes con Ownership
Retornar Múltiples Valores
fn main() {
let s1 = String::from("hola");
let (s2, longitud) = calcular_longitud(s1);
println!("La longitud de '{s2}' es {longitud}.");
}
fn calcular_longitud(s: String) -> (String, usize) {
let longitud = s.len(); // len() retorna la longitud de un String
(s, longitud) // Retornamos tanto el String como su longitud
}
Clonar para Mantener Ownership
fn main() {
let s1 = String::from("hola mundo");
// Clonar antes de pasar a la función
let longitud = calcular_longitud_clonando(s1.clone());
println!("'{s1}' tiene {longitud} caracteres");
// También podemos usar to_owned()
let palabras = contar_palabras(s1.to_owned());
println!("'{s1}' tiene {palabras} palabras");
}
fn calcular_longitud_clonando(s: String) -> usize {
s.len()
} // s sale de scope y se elimina (es una copia)
fn contar_palabras(s: String) -> usize {
s.split_whitespace().count()
} // s sale de scope y se elimina
Ownership con Colecciones
Vectores y Ownership
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// Tomar ownership del vector
let v2 = v; // v se mueve a v2
// println!("{:?}", v); // ERROR: value borrowed here after move
// Crear nuevo vector
let mut numeros = vec![10, 20, 30];
// Procesar sin mover
procesar_vector(numeros.clone());
println!("Original aún disponible: {:?}", numeros);
// Mover definitivamente
let resultado = tomar_vector(numeros);
println!("Resultado: {:?}", resultado);
// println!("{:?}", numeros); // ERROR: moved
}
fn procesar_vector(v: Vec<i32>) {
println!("Procesando: {:?}", v);
} // v se elimina aquí
fn tomar_vector(mut v: Vec<i32>) -> Vec<i32> {
v.push(40);
v
} // Retorna el vector modificado
HashMap y Ownership
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
// Tipos Copy se insertan sin problema
scores.insert(String::from("Azul"), 10);
scores.insert(String::from("Amarillo"), 50);
// Los String se mueven al HashMap
let field_name = String::from("Favorito");
let field_value = String::from("Azul");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name y field_value ya no son válidos
// Para mantener ownership, clona antes de insertar
let key = String::from("nuevo");
let value = String::from("valor");
map.insert(key.clone(), value.clone());
println!("Key: {key}, Value: {value}"); // Aún disponibles
println!("Mapa: {:?}", map);
}
Drop y RAII
El trait Drop permite personalizar qué ocurre cuando un valor sale de scope:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Eliminando CustomSmartPointer con data '{}'!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("mi dato"),
};
let d = CustomSmartPointer {
data: String::from("otro dato"),
};
println!("CustomSmartPointers creados.");
// Eliminar manualmente con drop()
drop(c); // Equivale a std::mem::drop(c)
println!("CustomSmartPointer eliminado antes del final de scope");
} // d se elimina automáticamente aquí
Ejercicios Prácticos
Ejercicio 1: Gestión de Strings
fn main() {
// Crear un string
let mensaje = crear_mensaje("Hola".to_string(), "Rust".to_string());
println!("Mensaje: {mensaje}");
// Procesar sin perder ownership
let (procesado, longitud) = procesar_mensaje(mensaje);
println!("Procesado: {procesado} (longitud: {longitud})");
// Usar el string procesado
mostrar_mensaje(procesado);
}
fn crear_mensaje(saludo: String, nombre: String) -> String {
format!("{saludo}, {nombre}!")
}
fn procesar_mensaje(mut msg: String) -> (String, usize) {
msg.push_str(" 🦀");
let len = msg.len();
(msg, len)
}
fn mostrar_mensaje(msg: String) {
println!("Mostrando: {msg}");
} // msg se elimina aquí
Ejercicio 2: Lista de Tareas
fn main() {
let mut tareas = vec![
String::from("Aprender Rust"),
String::from("Hacer ejercicios"),
String::from("Construir un proyecto"),
];
println!("Tareas originales:");
mostrar_tareas(tareas.clone()); // Clonar para mantener ownership
// Agregar nueva tarea
tareas = agregar_tarea(tareas, String::from("Compartir conocimiento"));
println!("\nDespués de agregar:");
mostrar_tareas(tareas.clone());
// Completar primera tarea
let (tareas_restantes, completada) = completar_primera_tarea(tareas);
println!("\nTarea completada: {completada}");
println!("Tareas restantes:");
mostrar_tareas(tareas_restantes);
}
fn mostrar_tareas(tareas: Vec<String>) {
for (i, tarea) in tareas.iter().enumerate() {
println!(" {}. {}", i + 1, tarea);
}
}
fn agregar_tarea(mut tareas: Vec<String>, nueva_tarea: String) -> Vec<String> {
tareas.push(nueva_tarea);
tareas
}
fn completar_primera_tarea(mut tareas: Vec<String>) -> (Vec<String>, String) {
let completada = tareas.remove(0);
(tareas, completada)
}
Ejercicio 3: Contador con Ownership
fn main() {
let contador = crear_contador(0);
println!("Contador inicial: {contador}");
let contador = incrementar(contador, 5);
println!("Después de incrementar 5: {contador}");
let contador = decrementar(contador, 2);
println!("Después de decrementar 2: {contador}");
let (contador, es_par) = verificar_paridad(contador);
println!("Contador: {contador}, ¿Es par? {es_par}");
}
fn crear_contador(inicial: i32) -> i32 {
inicial
}
fn incrementar(contador: i32, cantidad: i32) -> i32 {
contador + cantidad
}
fn decrementar(contador: i32, cantidad: i32) -> i32 {
contador - cantidad
}
fn verificar_paridad(contador: i32) -> (i32, bool) {
(contador, contador % 2 == 0)
}
Errores Comunes con Ownership
Error 1: Usar después de Mover
fn main() {
let s1 = String::from("hola");
let s2 = s1; // s1 se mueve a s2
// println!("{s1}"); // ERROR: borrow of moved value
// Solución: clonar
let s3 = String::from("mundo");
let s4 = s3.clone();
println!("{s3} {s4}"); // Ambos funcionan
}
Error 2: Retornar Referencias Locales
// ¡ESTO NO COMPILA!
// fn crear_string() -> &String {
// let s = String::from("hola");
// &s // ERROR: returns a reference to data owned by the current function
// }
// Solución: retornar el valor, no una referencia
fn crear_string() -> String {
String::from("hola") // Transfiere ownership
}
fn main() {
let s = crear_string();
println!("{s}");
}
Puntos Clave para Recordar
- Cada valor tiene un único propietario en cualquier momento
- Mover transfiere ownership, el valor original ya no es accesible
- Clonar crea una copia independiente pero es más costoso
- Los tipos Copy se duplican automáticamente (enteros, bool, char, etc.)
- Las funciones pueden tomar y retornar ownership
- Usar
drop() para eliminar valores manualmente
- RAII: los recursos se liberan automáticamente al salir de scope
- El compilador previene use-after-free y double-free automáticamente
Próximo Paso
El ownership puede ser restrictivo cuando solo quieres “prestar” un valor temporalmente. En el siguiente capítulo aprenderemos sobre Borrowing y Referencias, que nos permiten usar valores sin tomar su ownership.