Garantizando la Seguridad de la Memoria sin Recolector de Basura
En el contexto de un libro que explora las bases del lenguaje Rust, este capítulo aborda los mecanismos fundamentales de propiedad y préstamos, que constituyen el núcleo de su modelo de seguridad de memoria. Estos conceptos permiten a Rust prevenir errores comunes como el uso de memoria liberada o las carreras de datos sin necesidad de un recolector de basura, lo que resulta esencial para escribir código eficiente y seguro. Su comprensión es crucial para dominar la semántica del lenguaje y evitar frustraciones durante la compilación.
Semántica de Movimiento (Move)
La propiedad en Rust se basa en el principio de que cada valor tiene un único propietario, responsable de su ciclo de vida. Cuando un valor se asigna a una nueva variable o se pasa a una función, se produce un movimiento (move), transfiriendo la propiedad del valor original al nuevo contexto. Este mecanismo evita la duplicación implícita de datos, promoviendo la eficiencia, pero implica que el valor original deja de ser accesible.
Considérese el siguiente fragmento de código ilustrativo:
let s1 = String::from("hola");
let s2 = s1; // Movimiento: s1 ya no es válido
// println!("{}", s1); // Error: uso después de movimientoAquí, s1 transfiere su propiedad a s2, invalidando s1. Esto difiere de lenguajes como C++, donde una asignación podría copiar el valor si no se especifica lo contrario, o de Python, donde las asignaciones crean referencias compartidas. En Rust, el movimiento es la operación predeterminada para tipos no triviales, como String, que gestionan recursos en el heap.
Un caso borde surge con tipos que implementan el trait Drop, como vectores o cadenas: el movimiento transfiere no solo el valor, sino también la responsabilidad de liberar recursos. Si se intenta acceder al valor movido, el compilador emite un error en tiempo de compilación, previniendo usos dangling. Sin embargo, para tipos primitivos como i32, Rust realiza una copia implícita en lugar de un movimiento, ya que su representación es trivial y no gestiona recursos.
Otro detalle sutil: las funciones que toman un parámetro por valor provocan un movimiento, consumiendo el argumento. Por ejemplo:
fn consume(s: String) {
// s es ahora el propietario
}
let msg = String::from("mensaje");
consume(msg); // Movimiento: msg ya no accesibleEste comportamiento asegura que no haya aliasing no controlado, alineándose con el objetivo de Rust de eliminar errores de memoria en tiempo de ejecución.
El Trait Copy
No todos los tipos en Rust siguen la semántica de movimiento; aquellos que implementan el trait Copy se copian en lugar de moverse durante asignaciones o pases por valor. Este trait marca tipos cuya duplicación es barata y segura, típicamente primitivos o estructuras compuestas solo de tipos Copy. A diferencia de Clone, que permite duplicación explícita, Copy habilita copias implícitas.
Por ejemplo, los enteros son Copy por defecto:
let x: i32 = 42;
let y = x; // Copia: x sigue válido
println!("x: {}, y: {}", x, y); // Imprime: x: 42, y: 42En contraste, tipos como String no implementan Copy porque copiar implicaría una asignación costosa en el heap. Para derivar Copy en una estructura personalizada, todos sus campos deben ser Copy, y la estructura debe marcarse con #[derive(Copy, Clone)]. Un requisito formal es que el tipo no implemente Drop, ya que la copia implícita podría duplicar la liberación de recursos, violando la unicidad de propiedad.
Comparado con C, donde la copia de structs es siempre por valor pero puede ser costosa, Rust impone Copy solo para tipos eficientes, forzando movimientos para el resto. Un error típico ocurre al intentar derivar Copy en tipos con campos no Copy, lo que el compilador rechaza. Además, arrays de tamaño fijo son Copy si sus elementos lo son, pero vectores (Vec<T>) nunca lo son, promoviendo el uso de préstamos en lugar de copias.
Préstamos Inmutables
Para evitar movimientos constantes, Rust introduce los préstamos (borrowing), que permiten acceso temporal a un valor sin transferir propiedad. Un préstamo inmutable, denotado por &T, proporciona una referencia de solo lectura, permitiendo múltiples alias simultáneos sin mutación.
La sintaxis es directa:
fn longitud(s: &String) -> usize {
s.len() // Acceso inmutable
}
let texto = String::from("ejemplo");
let len = longitud(&texto); // Préstamo: texto sigue siendo propietarioAquí, &texto crea un préstamo inmutable, y la función accede al valor sin consumirlo. Esto contrasta con lenguajes como Java, donde las referencias son siempre mutables a menos que se especifique final, pero en Rust, la inmutabilidad es estricta: cualquier intento de mutar a través de un préstamo inmutable falla en compilación.
Un caso borde involucra préstamos anidados: un préstamo inmutable puede generarse a partir de otro, siempre que no se viole la inmutabilidad. Por ejemplo, &&String es válido y se resuelve a través de dereferenciación. Reglas formales incluyen que los préstamos inmutables no extienden la vida del valor prestado más allá de su propietario, aunque esto se gestiona implícitamente en contextos básicos.
Préstamos Mutables
Los préstamos mutables, indicados por &mut T, permiten modificación temporal del valor prestado, pero con restricciones estrictas para prevenir aliasing mutable. Solo un préstamo mutable puede existir a la vez para un valor dado, asegurando exclusividad.
Ejemplo ilustrativo:
fn agregar(s: &mut String) {
s.push_str(" mundo");
}
let mut saludo = String::from("hola");
agregar(&mut saludo); // Préstamo mutable: saludo se modificaTras el préstamo, saludo refleja los cambios. A diferencia de C++, donde punteros mutables pueden aliasarse inadvertidamente, Rust impone exclusividad: no se permiten préstamos inmutables concurrentes con un mutable. Intentar lo contrario genera un error del borrow checker.
Detalles sutiles incluyen la necesidad de que la variable propietaria sea declarada como mut para generar préstamos mutables. Además, en estructuras, préstamos mutables a campos específicos permiten mutaciones parciales sin afectar el resto. Un caso borde surge en bucles: un préstamo mutable persiste hasta el final de su ámbito, bloqueando otros accesos.
Reglas del Borrow Checker
El borrow checker es el componente del compilador de Rust responsable de validar las reglas de propiedad y préstamos en tiempo de compilación. Sus reglas formales son: (1) Cada valor tiene un único propietario. (2) Los préstamos no pueden outlive al propietario. (3) Múltiples préstamos inmutables son permitidos simultáneamente. (4) Solo un préstamo mutable es allowed a la vez, y no puede coexistir con préstamos inmutables.
Estas reglas se aplican estáticamente, analizando el flujo de control. Por ejemplo, en condicionales, el borrow checker verifica que no haya conflictos en ramas alternativas. Un detalle clave es la no-lexical lifetimes (NLL), que permiten que los préstamos terminen tan pronto como dejen de usarse, incluso dentro de un bloque.
Comparado con lenguajes como Go, que usan un GC para manejar aliasing, Rust’s borrow checker elimina la necesidad de runtime checks. Reglas adicionales incluyen que los préstamos a través de referencias no alteran la propiedad, y que dereferenciar (*) recupera el valor subyacente sin movimiento.
Errores Típicos
Entre los errores comunes relacionados con propiedad y préstamos se encuentran los usos después de movimiento, donde se accede a un valor transferido, resultando en errores como “value borrowed here after move”. Otro frecuente es el intento de múltiples préstamos mutables, violando la exclusividad y produciendo “cannot borrow as mutable more than once”.
Un error sutil ocurre con cierres que capturan por movimiento, consumiendo variables externas inadvertidamente. En contextos de iteración, prestar mutablemente un contenedor mientras se itera inmutablemente falla, ya que implica aliasing prohibido. Comparaciones con C revelan que errores similares allí causan undefined behavior en runtime, mientras que Rust los detecta temprano.
Otros casos incluyen olvidar declarar variables como mut para préstamos mutables, o intentar copiar tipos no Copy sin clonar explícitamente. El borrow checker también rechaza préstamos que outliven su fuente, aunque en fundamentos básicos esto se resuelve implícitamente.
Estos fundamentos de propiedad y préstamos sientan las bases para explorar mecanismos más avanzados de gestión de referencias y concurrencia en Rust, que se detallarán en capítulos posteriores para manejar escenarios donde las reglas básicas resultan insuficientes.