Lifetimes en structs: cuando un tipo guarda una referencia prestada
Si un struct almacena una referencia, debe declarar un lifetime: `struct Parser<'a> { input: &'a str }`. Qué significa que un tipo quede atado a un dato ajeno, por qué la instancia no puede sobrevivir a lo que presta, y cuándo conviene poseer en vez de prestar.
Hasta ahora las referencias vivían en variables y firmas. Pero un struct también puede guardar una referencia en un campo, y eso cambia la naturaleza del tipo: deja de ser un dato autónomo y se convierte en una vista atada a otro dato que vive fuera. Rust te obliga a declararlo con un lifetime —struct Parser<'a>—, y esa anotación arrastra una consecuencia inevitable: ninguna instancia del struct puede sobrevivir al dato que tiene prestado.
- Declarar un struct con un campo referencia usando un parámetro de lifetime.
- Entender que la instancia queda atada: no puede sobrevivir a lo que presta.
- Escribir
impl<'a>y métodos que devuelven referencias con el lifetime correcto. - Decidir entre prestar (
&'a str, cero copia) y poseer (String, autónomo).
Un campo referencia obliga a un lifetime
Si un struct contiene una referencia, el compilador exige que declares de qué región proviene. No puedes escribir input: &str a secas dentro de un struct: &str es &'a str, y ese 'a tiene que ser un parámetro del propio tipo.
struct Parser<'a> {
input: &'a str, // el struct NO posee el texto: lo mira prestado
pos: usize,
}
Omitir el 'a da un error inmediato, E0106 “missing lifetime specifier”, en la definición del campo. La razón es la misma contención de regiones de siempre, elevada al tipo: si el struct guarda una referencia, el compilador necesita un nombre para su región para poder garantizar, en cada uso del struct, que la instancia no sobrevive al input prestado.
Parser<'a> no fija ninguna región concreta: 'a es genérico, y lo instancia quien construye el Parser con la región del &str real que le pasa. El struct hereda la vida de su préstamo. No estás dando vida al texto desde el struct; estás declarando que el struct depende de una vida que ocurre fuera de él.
Un struct puede guardar varias referencias, y entonces admite varios lifetimes —uno por cada préstamo independiente—, igual que una función. Compartir un solo 'a entre todos los campos es más simple, pero también más restrictivo: obliga a que todos los datos prestados vivan al menos la misma región.
// Dos préstamos independientes: dos lifetimes distintos.
struct Entrada<'clave, 'valor> {
clave: &'clave str,
valor: &'valor str,
}
// con un solo 'a ambos campos quedarían atados a la MISMA región;
// con dos, cada campo puede provenir de un dato de vida distinta
La instancia queda atada a su dato
La consecuencia práctica es contundente: un Parser<'a> es tan efímero como el &'a str que guarda. Si el texto muere, el Parser deja de ser usable en ese mismo punto. El borrow checker lo impone comparando la región de la instancia con la de su campo prestado.
fn main() {
let parser;
{
let texto = String::from("clave=valor");
parser = Parser { input: &texto, pos: 0 }; // 'a = región de `texto`
// ... usar `parser` aquí sería válido
} // muere `texto`: su buffer se libera
// println!("{}", parser.input); // ERROR E0597: `texto` does not live long enough
}
flowchart LR texto[String texto es el dueno del buffer] -->|prestado en el campo input| parser[Instancia Parser con lifetime a] parser --> regla[La instancia no puede vivir mas que texto] regla --> err[Si lo intenta: E0597 does not live long enough]
Esta atadura tiene un efecto de diseño enorme: un struct con referencias no puede guardarse en estructuras de vida larga, ni devolverse desde una función que crea el dato prestado, ni almacenarse en un campo de otro objeto que le sobreviva. Es una vista temporal, y su lugar natural es la pila de llamadas cercana al dato original.
impl<'a> y métodos que devuelven referencias
Para dar métodos a un struct con lifetime, el bloque impl también declara el parámetro. La sintaxis repite 'a en tres sitios: tras impl, en el tipo, y donde haga falta en las firmas.
impl<'a> Parser<'a> {
fn nuevo(input: &'a str) -> Self {
Parser { input, pos: 0 }
}
// Devuelve una porción del input: vive tanto como el struct, es decir 'a.
fn resto(&self) -> &'a str {
&self.input[self.pos..]
}
}
Fíjate en el matiz de resto: devuelve &'a str, no una referencia atada a &self. La porción sale del input original, cuya región es 'a, así que puede sobrevivir a un &self efímero. Si hubieras escrito -> &str a secas, la elisión la habría atado a self (regla del self, lección 4), que es más restrictivo. Cuando la salida proviene del dato prestado y no del struct, anota 'a explícitamente.
Los structs prestados conviven sin fricción con los parámetros de tipo: un tipo puede llevar a la vez lifetimes y genéricos, y la convención es escribir primero los lifetimes y después los tipos. Así modelas una vista prestada sobre datos de cualquier tipo.
// Lifetime y tipo genérico juntos: los lifetimes van primero.
struct Ventana<'a, T> {
slice: &'a [T], // una vista prestada sobre elementos de tipo T
}
impl<'a, T> Ventana<'a, T> {
fn primero(&self) -> Option<&'a T> {
self.slice.first()
}
}
Struct que presta
Campo &'a T. Cero copia: mira datos que viven fuera. A cambio, la instancia es temporal y no puede sobrevivir a lo prestado.
Struct que posee
Campo String, Vec<T>, Box<T>. Autónomo: vive por su cuenta y va donde quieras. A cambio, paga la copia o la asignación en el heap.
Prestar o poseer: la decisión de diseño
La pregunta clave al modelar un struct con datos ajenos es si debe prestar o poseer. Prestar (&'a str) evita copiar y es la base de los parsers de cero copia, los iteradores y las vistas: rapidísimo, pero ata el tipo a la vida del dato. Poseer (String) hace el tipo autónomo y libre de lifetimes, a cambio de asignar y copiar. No hay respuesta universal; hay un compromiso entre rendimiento y libertad de vida.
// Versión que POSEE: sin lifetimes, autónoma, guardable en cualquier parte.
struct ParserDueno {
input: String, // el struct es dueño de su texto
pos: usize,
}
fn crear() -> ParserDueno {
ParserDueno { input: String::from("dato local"), pos: 0 }
// devolver esto es legal: no hay referencia que pueda colgar
}
Compara: ParserDueno puede devolverse desde crear sin problema, porque no cuelga de nada externo; el Parser<'a> prestado no podría devolverse apuntando a un String local, porque ese texto moriría al volver. La regla heurística: si el tipo va a vivir mucho, viajar lejos o guardarse en colecciones, que posea; si es una vista efímera junto al dato original y el rendimiento manda, que preste.
Un error clásico es intentar que un struct guarde a la vez un dato y una referencia a su propio campo —un String y un &str que apunte dentro de él—. No compila con lifetimes normales, y por una buena razón: si el struct se mueve en memoria, la referencia interna quedaría apuntando a la dirección vieja. Ese patrón autorreferencial es el territorio de Pin y de crates como ouroboros, mucho más adelante. Por ahora: un struct presta datos que viven en otro sitio, nunca en sí mismo.
Cuando pones un &'a str en un campo, no estás añadiendo una anotación local: estás inyectando un parámetro de región en el tipo mismo, y ese parámetro se propaga como una tinta que tiñe todo lo que toca. Cualquier struct que contenga un Parser<'a> deberá, a su vez, llevar un 'a y quedar atado a la misma región. Cualquier función que devuelva un Parser<'a> deberá relacionarlo con una entrada. El lifetime asciende por la jerarquía de composición como una obligación contagiosa: este árbol de tipos entero depende de un dato que vive fuera de él. Esto no es un defecto, es información de diseño valiosísima que otros lenguajes esconden. En Java o en Go, un objeto que guarda una referencia a otro no declara nada especial: parece autónomo, pero en realidad puede quedar apuntando a un objeto que ya nadie más usa —una fuga silenciosa— o forzar al recolector a mantener vivo un grafo enorme por una sola arista. Rust te obliga a hacer visible esa dependencia en el tipo, y con ella la pregunta que de verdad importa: ¿este dato es mío o solo lo estoy mirando? El coste es que a veces la tinta del lifetime se extiende más de lo que quisieras y acabas reescribiendo el campo como String. Pero esa fricción es diagnóstica: te está diciendo que habías diseñado un tipo que finge ser dueño de algo que solo tenía prestado. La solución idiomática casi nunca es luchar contra el lifetime, sino decidir la propiedad a conciencia: prestar cuando la vista es efímera y local, poseer cuando el tipo debe ser libre.
Un struct que guarda una referencia debe declarar su lifetime —struct Parser<'a> { input: &'a str }—, y con él la instancia queda atada: no puede sobrevivir al dato prestado (E0597 si lo intenta). Los métodos van en impl<'a>, y una salida que sale del dato prestado se anota &'a para no atarla al &self efímero. El lifetime se propaga por todo tipo que componga al struct. Ante la duda, decide con intención: prestar (&'a str) para vistas efímeras de cero copia, poseer (String) para tipos autónomos que viven y viajan libres.
- Define
Parser<'a>coninput: &'a str, constrúyelo dentro de un bloque y usa la instancia fuera del bloque: lee el E0597. - Escribe
impl<'a> Parser<'a>con un métodoresto(&self) -> &'a stry otro que devuelva algo atado a&self; explica la diferencia de vidas. - Reescribe el struct para que posea un
Stringy comprueba que ahora una función puede construirlo y devolverlo sin lifetimes. - Intenta guardar un
Parser<'a>como campo de otro struct sin propagar'ay lee el error; luego propágalo. - Argumenta en tres frases cuándo elegirías prestar y cuándo poseer para un tipo que representa una consulta a un texto grande.