Rust: ¿Propiedad o Conteo de Referencias?

Fuentes: Ownership & Borrowingversus Reference Counting

Rust, conocido por su seguridad de memoria sin necesidad de un recolector de basura, logra esto a través de un sistema de propiedad riguroso. Sin embargo, este sistema tiene una 'válvula de escape': el conteo de referencias. Normalmente, cada valor en Rust tiene un único propietario, y cuando ese propietario sale del ámbito, el valor se libera, liberando la memoria de forma determinista. El compilador Rust garantiza esto estáticamente, evitando errores comunes como punteros colgantes o doble liberación de memoria.

Pero, ¿qué pasa cuando necesitas compartir datos? Imagina un grafo donde múltiples aristas apuntan al mismo nodo, o una configuración que se utiliza en varios subsistemas. Aquí es donde entran en juego Rc<T> y Arc<T>, que trasladan la lógica de propiedad del tiempo de compilación a un contador en tiempo de ejecución.

El modelo de propiedad es la innovación central de Rust. La asignación en el heap tiene un único propietario. La propiedad puede moverse a otra variable, invalidando la original (a menos que el tipo implemente Copy). El 'borrow checker' de Rust aplica estas reglas en tiempo de compilación, previniendo errores de uso después de liberación.

Rust permite 'préstamos': referencias temporales y con ámbito limitado sin transferir la propiedad. El borrow checker impone reglas estrictas: solo puede haber un préstamo mutable (&mut T) a la vez, y los préstamos compartidos (&T) no pueden coexistir con préstamos mutables. Esto previene problemas como la invalidación de iteradores y las condiciones de carrera.

Rc<T> (Reference Counted) envuelve un valor en el heap junto con un contador de referencias. Cada clon de Rc<T> incrementa el contador; cada liberación lo decrementa. Cuando el contador llega a cero, el valor interno se libera. Arc<T> (Atomically Reference Counted) es una variante thread-safe de Rc<T>, utilizando operaciones atómicas para el contador.

Casos de uso: Rc<T> y Arc<T> son útiles en situaciones donde múltiples partes de un programa necesitan mantener un valor vivo, como grafos, árboles con nodos compartidos o configuraciones globales. Arc<T> es esencial para compartir datos entre hilos.

Consideraciones: Rc<T> y Arc<T> pueden generar ciclos de referencia, donde los valores se referencian mutuamente, impidiendo que sus contadores lleguen a cero y causando fugas de memoria. Para evitar esto, se utiliza Weak<T> para referencias 'débiles' que no impiden la liberación de memoria. También, Rc<T> tiene una sobrecarga de rendimiento (asignación de memoria adicional, indirección de punteros, incrementos/decrementos del contador), mientras que Arc<T> introduce la sobrecarga de operaciones atómicas. Elige Rc<T> o Arc<T> cuando la alternativa sea luchar contra el borrow checker o usar código unsafe.