wandres.dev
LIFETIMES · validez de las referencias

Elisión de lifetimes: las tres reglas que te ahorran escribirlos

Por qué casi nunca escribes lifetimes explícitos: las tres reglas de elisión que el compilador aplica mecánicamente a las firmas. Cómo funciona cada una, cuándo bastan para inferir la salida, y cuándo se quedan cortas y estás obligado a anotar a mano.

⏱ 16 min

Si los lifetimes están en toda referencia, ¿por qué escribes tan pocos? Porque el compilador aplica, sobre las firmas de las funciones, tres reglas de elisión que rellenan los lifetimes evidentes por ti. No son inferencia mágica ni analizan el cuerpo: son tres pasos mecánicos y deterministas que cubren la inmensa mayoría de las firmas reales. Entenderlas te dice exactamente cuándo no tienes que anotar nada —y, más útil aún, cuándo las reglas se quedan cortas y el compilador te exige la anotación explícita.

🎯 Al terminar esta lección sabrás
  • Enunciar las tres reglas de elisión de lifetimes que aplica el compilador.
  • Aplicarlas a mano a una firma para predecir si el compilador la acepta.
  • Identificar cuándo las reglas fallan y la anotación se vuelve obligatoria.
  • Saber que la elisión actúa en firmas; los cuerpos se infieren siempre por completo.

Las tres reglas

La elisión opera solo sobre lifetimes omitidos en la firma de una función o de un método. El compilador los rellena aplicando en orden estas tres reglas. Si al terminar todos los lifetimes de salida han quedado determinados, la firma compila sin que escribas nada; si alguno queda sin determinar, es un error y debes anotar.

1️⃣

Regla de entrada

Cada parámetro con lifetime omitido recibe uno propio y distinto: fn f(a: &A, b: &B) se lee fn f<'a, 'b>(a: &'a A, b: &'b B).

2️⃣

Regla de salida única

Si hay exactamente una entrada con lifetime, ese lifetime se asigna a todas las salidas omitidas.

3️⃣

Regla del self

Si hay &self o &mut self, el lifetime de self se asigna a todas las salidas omitidas, aunque haya más entradas.

La primera regla reparte lifetimes distintos a cada entrada. La segunda y la tercera son las que resuelven la salida: la segunda cuando solo hay una fuente posible, la tercera cuando el método tiene un receptor self que manda sobre los demás parámetros.

ℹ️
Escribir el lifetime que la elisión ya infiere es legal, solo redundante

Nada te impide anotar a mano el lifetime que las reglas habrían rellenado: fn primera_palabra<'a>(s: &'a str) -> &'a str compila igual que la versión elidida, y a veces es más claro en código didáctico. La elisión no prohíbe la anotación explícita, solo te ahorra escribirla en el caso común. Lo que nunca debes hacer es lo contrario: creer que la ausencia de 'a significa “sin lifetime”. El lifetime está ahí siempre; la elisión decide únicamente si lo ves escrito.

Regla 2 en acción: una sola entrada

El caso más común de todos. Una función con un único parámetro prestado que devuelve una referencia: no hay ambigüedad posible, la salida solo puede salir de esa entrada.

// Lo que escribes:
fn primera_palabra(s: &str) -> &str {
    match s.find(' ') { Some(i) => &s[..i], None => s }
}

// Lo que el compilador razona tras aplicar las reglas 1 y 2:
fn primera_palabra<'a>(s: &'a str) -> &'a str { /* ... */ }

La regla 1 le da a s el lifetime 'a; como es la única entrada con lifetime, la regla 2 asigna 'a a la salida. Firma resuelta, cero anotaciones. Este patrón —tomar una vista, devolver una subvista— es tan frecuente que la elisión lo vuelve invisible.

Regla 3 en acción: métodos con self

En un método, &self casi siempre es la fuente natural de las referencias que devuelves, así que la tercera regla lo prioriza sobre cualquier otro parámetro prestado.

struct Buffer { datos: String }

impl Buffer {
    // Escribes esto:
    fn buscar(&self, patron: &str) -> &str {
        match self.datos.find(patron) {
            Some(i) => &self.datos[i..],
            None => "",
        }
    }
    // El compilador lo lee como:
    // fn buscar<'s, 'p>(&'s self, patron: &'p str) -> &'s str
}

Aunque hay dos entradas prestadas —self y patron—, la regla 3 rompe el empate: la salida se ata a self. Esto es casi siempre lo que quieres, porque los métodos devuelven vistas de su propio contenido, no de sus argumentos.

La regla 3 cubre igual a &mut self: un método que devuelve un &mut de su propio contenido ata la salida a la región del receptor. Es lo que permite escribir getters mutables sin una sola anotación.

struct Pila { items: Vec<i32> }

impl Pila {
    // La regla 3 también gobierna &mut: la salida sale de `self`.
    fn cima_mut(&mut self) -> &mut i32 {
        self.items.last_mut().unwrap()   // -> &'s mut i32 por la regla del self
    }
}
flowchart TB
start[Firma con lifetimes omitidos] --> r1[Regla 1: un lifetime distinto por entrada]
r1 --> hay[Queda alguna salida sin lifetime?]
hay -->|no| ok[Firma resuelta: no anotas]
hay -->|si| r2[Hay una sola entrada con lifetime?]
r2 -->|si| asig1[Regla 2: esa entrada va a la salida]
r2 -->|no| r3[Hay self?]
r3 -->|si| asig2[Regla 3: el self va a la salida]
r3 -->|no| err[Ambiguo: E0106, anota a mano]
asig1 --> ok
asig2 --> ok

Cuándo las reglas fallan

La elisión es deliberadamente conservadora: solo resuelve lo que es inequívoco. En cuanto hay varias entradas prestadas y ningún self, ninguna regla decide de cuál sale la salida, y el compilador se rinde con E0106 en lugar de adivinar. Es el caso de mas_larga:

fn mas_larga(a: &str, b: &str) -> &str {   // ERROR E0106
    if a.len() >= b.len() { a } else { b }
}

La regla 1 da 'a a a y 'b a b. La regla 2 no aplica (hay dos entradas), la regla 3 tampoco (no hay self). La salida se queda huérfana de lifetime, y ahí terminan las reglas: debes anotar <'a> y decidir la relación. Que el compilador no adivine es una virtud, no una limitación: adivinar mal ataría la salida a un dato equivocado y escondería un error de vida bajo una firma que compila.

⚠️
La elisión no mira el cuerpo, solo la forma de la firma

Las tres reglas se aplican mirando únicamente la firma —número y forma de los parámetros—, jamás el cuerpo. Por eso mas_larga falla aunque el cuerpo devuelva siempre a: la elisión no lo sabe ni lo mira. Y por eso, dentro de una función, los lifetimes de las variables locales se infieren por completo con otro mecanismo (el análisis de regiones del checker), que sí analiza el flujo. Elisión = azúcar sintáctico en firmas; inferencia de cuerpos = análisis de flujo. No los confundas.

Conviene fijar el alcance de la elisión: actúa en las firmas de funciones y métodos, no en cualquier posición. Los campos de un struct nunca la disfrutan —siempre declaras su lifetime, como viste en la lección 3—, y los ítems static y const que contienen referencias exigen 'static explícito. La elisión es una comodidad para el caso rutinario de las funciones, no una ley universal del lenguaje; fuera de las firmas, sigues escribiendo la región a mano.

'_ y la elisión explícita

A veces quieres señalar que hay un lifetime elidido sin nombrarlo, sobre todo en tipos con lifetime como Parser<'a>. Para eso está '_, el lifetime anónimo: le dice al lector y al compilador “aquí hay una región, deja que la elisión la resuelva”. En Rust 2024 es idiomático y en muchos contextos el lint elided_lifetimes_in_paths te anima a escribirlo.

struct Parser<'a> { input: &'a str }

// `'_` hace visible que Parser lleva un lifetime, resuelto por elisión:
fn crear(texto: &str) -> Parser<'_> {
    Parser { input: texto }
}
// aplicando las reglas: fn crear<'a>(texto: &'a str) -> Parser<'a>

El '_ también aparece en objetos de trait y en tipos con lifetime dentro de rutas: escribir Box<dyn Error + '_> o Ref<'_, T> hace visible que ahí vive una región elidida. En la edición 2024, el estilo idiomático prefiere el '_ explícito a la omisión total, porque le avisa al lector de que el tipo carga un lifetime sin obligarte a bautizarlo. Es la mejor de las dos posturas: la comodidad de no inventar un nombre y la honestidad de no esconder que hay una región en juego.

La elisión codifica el caso común para que la anotación marque lo excepcional

Detrás de las tres reglas hay una filosofía de diseño de lenguajes que merece admiración. Los diseñadores de Rust hicieron un estudio empírico de firmas reales y descubrieron que la abrumadora mayoría cae en dos patrones: una función que transforma una entrada prestada en una salida prestada, o un método que devuelve una vista de self. Las reglas 2 y 3 son, literalmente, esos dos patrones convertidos en inferencia automática. La consecuencia es profunda: en Rust, escribir un lifetime explícito es una señal. Cuando ves un 'a anotado a mano, el autor te está diciendo “atención, aquí la relación temporal no es la obvia: hay varias fuentes y he elegido esta”. La anotación se reserva para lo excepcional precisamente porque la elisión absorbe lo rutinario. Compáralo con un mundo sin elisión, donde cada firma llevaría lifetimes: el ruido ahogaría la señal, y un 'a no distinguiría lo trivial de lo delicado. Este es un principio general del buen diseño de notación —haz que lo común sea silencioso y lo raro, ruidoso— aplicado con rigor a la seguridad temporal. Y tiene una lección práctica: si te ves obligado a anotar, no lo vivas como una derrota ante el compilador, sino como el momento en que tu función abandona el caso común y necesitas pensar la relación de vidas. Las reglas de elisión no te ocultan los lifetimes; te enseñan cuáles merecen tu atención.

📝
Lo esencial de la elisión

Tres reglas mecánicas rellenan los lifetimes omitidos en las firmas: (1) cada entrada recibe uno propio; (2) si hay una sola entrada con lifetime, va a la salida; (3) si hay &self, el de self va a la salida. Si tras aplicarlas queda alguna salida sin lifetime —varias entradas y ningún self—, es E0106 y anotas a mano. La elisión mira solo la forma de la firma, nunca el cuerpo; los cuerpos se infieren aparte por análisis de flujo. Usa '_ para hacer visible un lifetime elidido en tipos como Parser<'_>.

⚔️ Aplica las reglas a mano
  1. Coge cinco firmas con referencias y, para cada una, aplica las tres reglas en orden y predice si compila antes de probarla.
  2. Explica por qué fn primera_palabra(s: &str) -> &str no necesita anotación pero fn mas_larga(a: &str, b: &str) -> &str sí.
  3. Escribe un método -> &str que devuelva parte de self y comprueba que la regla 3 lo ata a self aunque tenga otro parámetro prestado.
  4. Fuerza el fallo de elisión con dos entradas prestadas sin self, lee el E0106 y arréglalo anotando.
  5. Sustituye un lifetime explícito por '_ en un retorno Parser<'_> y razona qué regla lo resuelve.