wandres.dev
LIFETIMES AVANZADOS · los casos difíciles

Lifetimes en trait objects: `dyn Trait + 'a` y el `'static` implicito

Todo objeto de trait carga un lifetime oculto que acota cuanto viven las referencias del tipo borrado. `Box<dyn Trait>` no es neutro: significa `Box<dyn Trait + 'static>` por un juego de defectos preciso. Como leer esos defectos, por que `Box` asume `'static` mientras `&'a dyn Trait` asume `'a`, y como relajarlos con `+ 'a` para almacenar objetos que toman prestado.

⏱ 18 min

Cuando borras un tipo detras de dyn Trait, el compilador pierde de vista su forma concreta —y con ella, cuanto viven las referencias que ese tipo pudiera guardar dentro—. Para no perder la seguridad, cada objeto de trait carga un lifetime implicito que acota exactamente eso: dyn Trait + 'a promete que el tipo borrado no contiene ninguna referencia mas corta que ’a. Lo desconcertante es que ese lifetime casi nunca se escribe, y sus defectos cambian segun el contenedor: Box<dyn Trait> asume 'static, pero &'a dyn Trait asume 'a. Esta leccion descifra esos defectos, explica por que existen, y te ensena a relajarlos para almacenar objetos que toman prestado de su entorno.

🎯 Al terminar esta lección sabrás
  • Leer dyn Trait + 'a como “el tipo borrado sobrevive a ’a” y entender por que el borrado de tipos exige esa cota.
  • Enunciar los defectos de lifetime de objetos: 'static para Box<dyn Trait>, 'a para &'a dyn Trait.
  • Relajar el defecto con + 'a o + '_ para guardar objetos de trait que contienen referencias efimeras.
  • Aplicarlo al caso practico de almacenar closures prestados en Box<dyn Fn() + 'a>.

Por que un objeto de trait necesita un lifetime

Un Box<dyn Dibuja> puede esconder cualquier tipo que implemente Dibuja: uno que posea todos sus datos, o uno que guarde una referencia prestada. El compilador, tras el borrado, no distingue cual es —esa es toda la gracia del despacho dinamico—. Pero entonces surge un peligro: si el tipo oculto guardaba un &'corto str, el objeto no puede sobrevivir a esa region, o accederiamos a memoria muerta a traves de la vtable. La cota + 'a es la promesa que cierra ese agujero: el tipo borrado sobrevive al menos a ’a, formalmente ConcretoBorrado: 'a.

trait Dibuja { fn render(&self) -> String; }

// Estas dos anotaciones dicen cosas distintas sobre el tipo escondido:
fn _a(_x: Box<dyn Dibuja + 'static>) {}   // el tipo borrado no guarda prestamos efimeros
fn _b<'a>(_x: Box<dyn Dibuja + 'a>) {}     // el tipo borrado puede prestar, pero sobrevive a 'a

Que ese + 'a sea parte del tipo del objeto, y no un detalle interno, es lo que permite al borrow checker seguir razonando sobre vidas incluso despues de haber perdido el tipo concreto. El borrado esconde la forma, pero nunca la region.

Los defectos: por que Box<dyn Trait> es 'static

Casi nunca escribes ese + 'a, y sin embargo esta siempre. La razon es un conjunto de reglas de lifetime por defecto para objetos de trait, distintas de la elision de funciones. Se aplican en este orden:

📦

Contenedor sin lifetime: 'static

Box<dyn Trait>, Arc<dyn Trait>, Rc<dyn Trait>: el constructor no aporta ninguna region, asi que el defecto es + 'static.

🔖

Referencia: la region de la referencia

&'a dyn Trait se lee &'a (dyn Trait + 'a); &'a mut dyn Trait igual. El objeto hereda la region del prestamo que lo envuelve.

🏷️

Tipo con un lifetime unico: ese

Ref<'a, dyn Trait> toma 'a del contenedor cuando este tiene exactamente un parametro de lifetime.

El caso que mas sorprende es el primero. Box<dyn Trait> no es “un objeto de vida cualquiera”: es Box<dyn Trait + 'static>, la restriccion mas fuerte posible. La logica del defecto es la prudencia: un Box puede moverse a cualquier parte, devolverse de una funcion, guardarse en una estructura de larga vida; sin ninguna region del contexto que lo limite, el compilador asume lo unico universalmente seguro —que el tipo borrado no depende de ningun prestamo efimero—. Por eso un Box<dyn Trait> que intente contener una referencia local falla, y el mensaje habla de 'static aunque tu no lo escribieras.

Ver los tres defectos escritos como los desazucara el compilador disuelve casi toda la confusion:

// Lo que escribes    ->    lo que el compilador entiende
//  Box<dyn Dibuja>         Box<dyn Dibuja + 'static>       (contenedor sin region: static)
//  &'a dyn Dibuja          &'a (dyn Dibuja + 'a)           (hereda la region del prestamo)
//  &dyn Dibuja             &'a (dyn Dibuja + 'a)           (misma regla con 'a elidido)
//  Rc<dyn Dibuja>          Rc<dyn Dibuja + 'static>        (igual que Box)
trait Dibuja { fn render(&self) -> String; }
struct Etiqueta<'a> { texto: &'a str }
impl<'a> Dibuja for Etiqueta<'a> {
    fn render(&self) -> String { self.texto.to_string() }
}

fn en_caja_mal(s: &str) -> Box<dyn Dibuja> {   // = Box<dyn Dibuja + 'static>: ERROR
    Box::new(Etiqueta { texto: s })            // Etiqueta<'_> presta de s, no es 'static
}

Relajar el defecto: + 'a para objetos que prestan

La cura no es forzar 'static sobre los datos ni clonar a lo bruto: es relajar el defecto declarando la region real del objeto. Anadiendo + 'a al tipo del objeto le dices al compilador “este objeto puede prestar, pero yo garantizo que vive dentro de ’a”.

// Relajado: el objeto puede contener referencias, atadas a la region 'a de la entrada.
fn en_caja_bien<'a>(s: &'a str) -> Box<dyn Dibuja + 'a> {
    Box::new(Etiqueta { texto: s })   // ahora el + 'a admite el prestamo de s
}

El caso practico por excelencia son los closures que capturan por referencia. Un Box<dyn Fn()> es Box<dyn Fn() + 'static> y rechaza capturar prestamos locales; para almacenar una devolucion de llamada que toma prestado de su entorno, anotas + 'a (o + '_, dejando que la region se infiera del contexto).

fn hacer_callback<'a>(datos: &'a [i32]) -> Box<dyn Fn() -> usize + 'a> {
    Box::new(move || datos.len())   // captura &'a [i32]; el + 'a lo autoriza
}
flowchart TB
o[dyn Trait sin lifetime escrito] --> c[En que contenedor esta?]
c -->|Box, Arc, Rc| s[Defecto static: no puede prestar]
c -->|ref con vida a| r[Defecto a: hereda la region del prestamo]
c -->|tipo con un solo lifetime| t[Defecto: ese lifetime]
s --> need[El objeto necesita prestar?]
need -->|si| relax[Relaja escribiendo mas a en el tipo del objeto]
need -->|no| ok[static esta bien]
ℹ️
El `+ 'a` del objeto no es lo mismo que un supertrait `Trait: 'a`

Distingue dos anotaciones que se parecen. Box<dyn Trait + 'a> acota el tipo borrado en ese uso concreto: es una propiedad del objeto, no del trait. En cambio trait Trait: 'a (un supertrait de lifetime) obligaria a toda implementacion del trait a satisfacer esa cota, en todas partes. El primero es local y flexible, lo eliges caso por caso; el segundo es global y raro, casi nunca lo quieres. Cuando ves + 'a junto a un dyn, piensa “region de este objeto aqui”, no “requisito del trait en general”.

El defecto 'static es la conservacion de la seguridad a traves del borrado de tipos

Hay una simetria profunda que explica por que Box<dyn Trait> asume 'static mientras &'a dyn Trait asume 'a, y entenderla ilumina como Rust preserva su garantia central incluso cuando renuncia a conocer los tipos. El borrado de tipos es, literalmente, una perdida de informacion: cambias un tipo concreto por una vtable y olvidas todo lo demas. Pero la seguridad de memoria de Rust es un invariante que no puede perderse, pase lo que pase con los tipos. La pregunta de diseno es entonces: cuando olvido el tipo concreto de un objeto, que debo asumir sobre las referencias que pudiera contener para no romper nunca la seguridad? Y la respuesta depende de una sola cosa: cuanta informacion del contexto sobrevive al borrado. Cuando el objeto viaja dentro de un &'a —una referencia que ya carga su region en el tipo—, esa region es informacion viva que el compilador puede reutilizar: hereda 'a y punto, porque la referencia externa ya limita cuanto puede vivir el objeto. Pero cuando el objeto viaja dentro de un Box, el contenedor no aporta ninguna region: un Box<T> es generico sobre T sin ninguna cota de vida, capaz de ir a cualquier parte y durar lo que sea. Sin ninguna region del contexto que lo ate, la unica asuncion que jamas puede fallar es la mas restrictiva: que el tipo borrado no dependa de nada efimero, es decir, 'static. El defecto no es arbitrario ni conservador por pereza: es el punto fijo de un razonamiento de seguridad. Elige, para cada contenedor, la cota mas debil que sigue siendo imposible de violar dada la informacion que sobrevive al borrado. Esta es la marca del diseno de Rust en su forma mas pura: cuando el lenguaje debe elegir un defecto en ausencia de informacion, no elige lo comodo ni lo probable, elige lo que preserva el invariante en el peor caso, y te ofrece una valvula explicita —el + 'a— para relajarlo cuando tu sabes mas que el. La leccion trasciende los trait objects. Cada vez que una abstraccion oculta informacion, alguien debe decidir que asumir sobre lo oculto, y solo hay una eleccion que nunca traiciona la seguridad: la mas pesimista compatible con lo que aun se sabe. Box<dyn Trait> siendo 'static por defecto es esa filosofia grabada en la sintaxis mas cotidiana del lenguaje.

📝
Lo esencial de los lifetimes en trait objects

Todo dyn Trait carga un lifetime implicito + 'a que promete que el tipo borrado sobrevive a ’a; sin el, el borrado de tipos podria esconder una referencia colgante. Los defectos dependen del contenedor: Box<dyn Trait>, Arc, Rc asumen 'static (no pueden prestar); &'a dyn Trait hereda 'a; un tipo con un unico lifetime aporta el suyo. Para almacenar objetos que si prestan —una Etiqueta<'a> o un closure que captura por referencia— relaja el defecto escribiendo Box<dyn Trait + 'a> o Box<dyn Fn() + 'a>. El + 'a acota el objeto en ese uso, no es un supertrait sobre el trait entero. El defecto 'static de Box es la asuncion mas pesimista que preserva la seguridad cuando el contenedor no aporta ninguna region.

⚔️ Domina el lifetime oculto del dyn
  1. Escribe struct Etiqueta<'a> que implemente un trait Dibuja, e intenta devolverla como Box<dyn Dibuja>; lee el error que menciona 'static y arreglalo con Box<dyn Dibuja + 'a>.
  2. Verifica que &dyn Dibuja en una firma se lee &'a (dyn Dibuja + 'a) construyendo una funcion que preste un objeto de trait y razonando por que no exige 'static.
  3. Crea fn callback<'a>(v: &'a [i32]) -> Box<dyn Fn() -> usize + 'a> que capture v por referencia; quita el + 'a y explica por que deja de compilar.
  4. Compara Box<dyn Fn()> con Box<dyn Fn() + '_> en un retorno y razona que aporta el '_ frente al defecto 'static.
  5. Contrasta escribir + 'a en un objeto concreto con declarar un supertrait trait Dibuja: 'a; describe a que codigo afecta cada uno y por que el primero es la herramienta habitual.