En Rust, cuando necesitamos valores que permanezcan inmutables durante toda la ejecución, utilizamos const o static. Aunque ambos representan datos que no cambian, su funcionamiento a nivel de memoria es fundamentalmente distinto.

const define una constante que es evaluada totalmente en tiempo de compilación. El compilador no asigna una dirección de memoria única para la constante; en su lugar, “incrusta” o copia el valor directamente en cada lugar del código donde se utiliza. Debido a esto, las constantes requieren siempre una anotación de tipo explícita y su valor debe ser una “expresión constante” (operaciones que el compilador pueda resolver sin ejecutar el programa, como operaciones aritméticas simples). Por convención, siempre se deben nombrar usando SCREAMING_SNAKE_CASE.

static declara una variable global con una dirección de memoria fija que persiste durante toda la vida del programa (lifetime 'static). A diferencia de const, un static no se copia cada vez que se usa; todas las referencias apuntan al mismo punto en la memoria. Esto es útil cuando necesitas que un valor sea único en todo el programa o cuando el tamaño del dato es muy grande y no quieres duplicar su peso en el binario.

Es importante mencionar las const fn, que son funciones especiales que pueden ejecutarse durante la compilación, permitiendo inicializar constantes mediante lógica de programación. Por último, aunque Rust permite static mut (estáticos mutables), su uso es unsafe debido al riesgo de condiciones de carrera en entornos multihilo. La práctica recomendada para estado global mutable es utilizar tipos atómicos (AtomicUsize, etc.) o Mutex<T>.

use std::sync::atomic::{AtomicUsize, Ordering};

// Constante: el valor se incrusta directamente donde se usa
const LIMITE_MAXIMO: u32 = 100;

// Función constante: permite realizar cálculos en tiempo de compilación
const fn calcular_doble(n: u32) -> u32 {
    n * 2
}

// Estática: tiene una dirección de memoria fija y única
static VERSION_APP: &str = "1.0.0";

// Estática atómica: la forma segura de manejar estado global mutable
static CONTADOR_GLOBAL: AtomicUsize = AtomicUsize::new(0);

fn main() {
    // Uso de la constante
    let valor = LIMITE_MAXIMO;
    let doble = calcular_doble(valor);

    // Uso de la estática (referencia a memoria fija)
    println!("Versión de la app: {}", VERSION_APP);
    println!("El doble de {} es: {}", LIMITE_MAXIMO, doble);

    // Modificación segura del estado global usando tipos atómicos
    CONTADOR_GLOBAL.fetch_add(1, Ordering::SeqCst);
    
    let valor_actual = CONTADOR_GLOBAL.load(Ordering::SeqCst);
    println!("El contador global actual es: {}", valor_actual);
}

Explicación del Código

  • const LIMITE_MAXIMO: u32: Define una constante de tipo u32. Su valor 100 se sustituirá directamente en el código cada vez que se mencione.
  • const fn calcular_doble(n: u32) -> u32: Es una función constante que toma un u32 y devuelve otro u32. Al ser const fn, el compilador puede resolver calcular_doble(LIMITE_MAXIMO) durante la compilación.
  • static VERSION_APP: &str: Declara una referencia estática a una cadena de texto. Esta variable reside en una ubicación fija de la memoria durante toda la ejecución.
  • static CONTADOR_GLOBAL: AtomicUsize: Define un contador global utilizando AtomicUsize. Al ser un tipo atómico, permite modificar su valor de forma segura entre múltiples hilos sin usar bloques unsafe.
  • CONTADOR_GLOBAL.fetch_add(1, Ordering::SeqCst): Incrementa el valor de CONTADOR_GLOBAL en 1. Ordering::SeqCst es el nivel de ordenamiento más estricto para asegurar la consistencia en la memoria.
  • CONTADOR_GLOBAL.load(Ordering::SeqCst): Recupera el valor actual almacenado en CONTADOR_GLOBAL.

12