'static y los patrones de lifetimes del día a día
'static es la región del programa entero, pero tiene dos significados que se confunden: `&'static T` (una referencia válida siempre) y `T: 'static` (un tipo sin referencias efímeras). Los patrones y errores de lifetimes que de verdad aparecen a diario, y cómo resolverlos sin sobre-restringir.
'static es el lifetime más largo posible: la región que abarca todo el programa. Pero es también el más malentendido de Rust, porque significa dos cosas distintas según dónde aparezca: como referencia, &'static T promete un dato válido hasta el final del proceso; como cota, T: 'static promete un tipo que no contiene referencias efímeras. Esta lección cierra el nivel destilando 'static y catalogando los patrones y errores de lifetimes que de verdad te encontrarás escribiendo Rust.
- Distinguir los dos
'static: la referencia&'static Ty la cotaT: 'static. - Reconocer las fuentes de
&'static: literales,static, y datos filtrados. - Resolver los errores frecuentes: devolver un local, atar de más, hilos.
- Adoptar la heurística: no sobre-restringir con
'staticpara callar al checker.
Los dos significados de 'static
Aquí está la confusión que hay que disolver de una vez. 'static aparece en dos posiciones gramaticalmente distintas, y significan cosas diferentes.
&'static T · la referencia
Una referencia cuya región es el programa entero: el dato al que apunta vive hasta que termina el proceso. Ejemplo: un literal de cadena.
T: 'static · la cota
Un tipo que no contiene ninguna referencia con vida más corta que el programa. Puede ser dueño de todo (String) o no tener referencias en absoluto.
La trampa: String cumple String: 'static aunque no viva “para siempre” —un String local se libera al salir de scope—. La cota T: 'static no dice “vive eternamente”; dice “podría vivir hasta el final del programa porque no depende de ningún préstamo más corto”. Un tipo que posee todos sus datos siempre satisface 'static; solo lo incumple un tipo que guarda una referencia efímera, como Parser<'a> con 'a distinto de 'static.
fn exige_static<T: 'static>(_valor: T) {}
fn main() {
let s = String::from("dueño de sus bytes");
exige_static(s); // OK: `String: 'static`, aunque `s` sea local y efímero
exige_static(42_i32); // OK: un i32 no contiene referencias
let texto = String::from("...");
let vista: &str = &texto;
// exige_static(vista); // ERROR: `&str` con lifetime local NO es 'static
}
La cota T: 'static es una de las más frecuentes de las bibliotecas: la piden thread::spawn, Box<dyn Any>, muchos canales y casi todo lo que guarda un valor por tiempo indefinido. El sentido es siempre el mismo: “no me des algo que dependa de un préstamo que pueda caducar antes que yo”. Un valor que posee sus datos la cumple sin esfuerzo; uno que guarda una referencia efímera, nunca. Por eso, cuando una API te exige 'static, la respuesta idiomática casi nunca es forzar el lifetime, sino entregar la propiedad —clonar a un String, envolver en un Arc— para que el valor deje de depender de nada externo.
De dónde salen las &'static
Referencias genuinamente &'static hay tres fuentes principales. La más común, y la que ya usabas sin saberlo: los literales de cadena. Todo "texto" incrustado en el código vive en el binario y es un &'static str.
let saludo: &'static str = "hola"; // el literal vive en el binario: 'static
static VERSION: &str = "1.0"; // un `static` también dura todo el programa
const MAX: u32 = 100; // las const se materializan donde se usan
La segunda fuente son los static: un ítem static ocupa una dirección fija durante toda la ejecución, así que las referencias a él son &'static. La tercera, más avanzada, es filtrar memoria a propósito con Box::leak, que renuncia a liberar un Box y te devuelve un &'static mut T válido para siempre —útil en arranque de programas, peligroso como costumbre—.
fn config_global() -> &'static str {
let cargada = String::from("valor leído al arrancar");
Box::leak(cargada.into_boxed_str()) // renuncia a liberar: la vista vive todo el programa
}
Box::leak convierte propiedad en una &'static: cedes la liberación a cambio de una referencia válida hasta el final del proceso. Es legítimo para inicializar un recurso global una vez, pero veneno como costumbre, porque cada fuga es memoria que ya no vuelve.
Una &'static str puede usarse durante todo el programa, pero no está obligada a hacerlo: una variable que la contiene se libera al salir de scope como cualquier otra —lo que se libera es la referencia, de 16 bytes, no el dato al que apunta—. Y T: 'static no habla de duración real, sino de ausencia de préstamos cortos. Traducir 'static como “eterno” es la raíz de la mitad de las confusiones. Léelo siempre como “su región es, como mucho, el programa entero” y “no cuelga de ningún dato más efímero”.
Los errores que aparecerán de verdad
Tres patrones concentran la mayoría de las peleas con lifetimes en el día a día. Conócelos y la mayoría de tus errores se resolverán en segundos.
Devolver una referencia a un local (E0515). El más frecuente. La cura nunca es forzar 'static, sino devolver la propiedad.
fn mal() -> &'static str {
let local = String::from("efímero");
// &local // ERROR E0515: cannot return reference to local variable
"literal" // esto sí: un literal es de verdad 'static
}
fn bien() -> String {
String::from("devuelvo el dueño, no una referencia") // sin lifetimes, sin drama
}
Hilos y 'static (el spawn). thread::spawn exige que el closure sea 'static: como el hilo puede vivir más que quien lo lanzó, no puede capturar referencias efímeras. La solución idiomática es move para poseer lo capturado, o Arc<T> para compartir propiedad.
use std::thread;
use std::sync::Arc;
fn main() {
let datos = Arc::new(vec![1, 2, 3]);
let copia = Arc::clone(&datos);
thread::spawn(move || { // `move`: el closure posee `copia`
println!("{:?}", copia); // Arc<Vec<_>>: 'static, sin préstamos cortos
});
}
Objetos de trait y su lifetime por defecto. Un Box<dyn Trait> es en realidad Box<dyn Trait + 'static> por defecto. Si el objeto guarda referencias efímeras, debes anotarlo: Box<dyn Trait + 'a>.
trait Dibuja { fn render(&self) -> String; }
// Por defecto el objeto de trait es 'static: no captura préstamos cortos.
fn en_caja(x: Box<dyn Dibuja>) -> Box<dyn Dibuja> { x } // = Box<dyn Dibuja + 'static>
// Si el objeto sí guarda referencias efímeras, declara su región con '_ o 'a:
fn prestado<'a>(x: Box<dyn Dibuja + 'a>) -> Box<dyn Dibuja + 'a> { x }
flowchart TB err[Error de lifetime] --> tipo[Que forma tiene?] tipo -->|devuelvo referencia a un local| own[Devuelve el dueno por valor] tipo -->|closure a otro hilo| mv[Usa move o Arc para poseer] tipo -->|struct guarda una vista| dec[Decide: prestar con lifetime o poseer] own --> fin[Resuelto sin sobre-restringir] mv --> fin dec --> fin
No sobre-restrinjas: 'static no es un martillo
El antipatrón más dañino del principiante es sembrar 'static para acallar al compilador. Casi siempre es un diagnóstico equivocado: 'static es una restricción fortísima —exige que el dato viva todo el programa o que el tipo no tenga préstamos cortos— y usarla como parche convierte una firma flexible en una rígida que rechaza usos legítimos.
Cuando el checker se queje, recorre esta escalera antes de pensar en 'static. Primero: ¿puedes devolver o guardar el dueño (String, Vec<T>) en vez de una referencia? Elimina el lifetime de raíz. Segundo: ¿el problema es una firma con varias entradas? Ata bien los 'a a la entrada correcta (lección 2). Tercero: ¿es un struct que presta? Decide entre &'a y poseer (lección 3). Solo si el dato es de verdad global —una configuración cargada al inicio, un literal, un recurso filtrado a propósito— usa 'static. Sembrar 'static para callar el error es como recetar antibiótico para todo: a veces funciona por casualidad, casi siempre esconde el diagnóstico real.
Termina de recomponer el nivel entero con esta imagen. Todas las regiones de un programa forman un orden parcial por inclusión: una región 'a es “menor o igual” que 'b —escrito 'b: 'a— cuando 'a está contenida en 'b. En ese orden, el punto ínfimo es una región de un solo uso —una referencia que nace y muere en la misma expresión— y el punto supremo, el techo absoluto, es 'static: la región que contiene a todas las demás porque abarca el programa completo. Toda la maquinaria que has visto en este nivel es aritmética en esta jerarquía. La contención de regiones de la lección 1 es la relación de orden. La anotación <'a> de la lección 2 es cuantificar sobre este retículo: “para toda región 'a…”. La elisión de la lección 4 es rellenar los puntos del orden que son inequívocos. Y 'static es, sencillamente, el máximo. Por eso T: 'static es la cota más fuerte posible sobre un tipo: exige que ningún préstamo interno lo ancle por debajo del techo. Y por eso es tan mal parche: pedir 'static es exigir el elemento máximo del retículo cuando casi siempre te bastaba con cualquier región suficientemente grande, mucho más abajo. El programador experto no piensa en lifetimes como sintaxis a memorizar, sino como puntos en un orden: dado un dato, ¿cuál es la región más pequeña que necesito? ¿qué otras regiones la contienen? ¿estoy pidiendo el techo cuando me sobra con una habitación? Cuando ves los lifetimes así —un retículo de regiones donde 'static es el cielo y una expresión es el suelo—, dejan de ser el obstáculo temido de Rust y se revelan como lo que siempre fueron: la geometría del tiempo en tu programa, hecha explícita para que el compilador la demuestre por ti.
'static es la región del programa entero, con dos caras: &'static T es una referencia válida siempre (literales, static, datos filtrados); T: 'static es un tipo sin referencias efímeras (todo dueño, como String, la cumple). No significa “eterno”, sino “no cuelga de nada más corto”. Los tres errores diarios —devolver un local (E0515), closures a otro hilo, structs que prestan— se curan poseyendo o atando bien, casi nunca con 'static. Sembrar 'static para callar al checker pide el techo del retículo de regiones cuando te sobraba con mucho menos.
- Escribe
fn exige_static<T: 'static>(x: T)y pásale unString, uni32y un&strcon lifetime local; explica cuál falla y por qué, pese a que elStringtambién es local. - Anota una
&'static strcon un literal y demuestra que la variable que la guarda igual se libera al salir de scope. - Reproduce el E0515 devolviendo
&localy arréglalo de dos formas: devolviendo un literal y devolviendo elStringpor valor. - Lanza un
thread::spawnque capture unVecpor referencia, lee el error de'static, y arréglalo conmovey conArc. - Toma una firma donde alguien puso
'staticde más y relájala a un'agenérico; razona qué llamadas nuevas pasan a compilar.