Elision avanzada: donde el compilador se rinde y te obliga a anotar
Las tres reglas de elision cubren el caso comun, pero fallan de formas predecibles: varias entradas prestadas sin `self`, una salida que quieres atar a un parametro y no al receptor, y sobre todo la captura de lifetimes en `impl Trait` de retorno, que en la edicion 2024 cambio de raiz. Cuando y por que estas obligado a escribir la region a mano.
Ya sabes que tres reglas mecanicas rellenan los lifetimes omitidos en las firmas y por eso escribes tan pocos. Esta leccion mira el otro lado: los limites de esa maquinaria. La elision es deliberadamente conservadora —solo resuelve lo inequivoco— y hay familias enteras de firmas donde se rinde y te exige la anotacion explicita. Algunas son clasicas (dos entradas prestadas sin self). Otras son sutiles y aparecen al devolver impl Trait, donde la captura de lifetimes cambio de raiz en la edicion 2024. Conocer donde el compilador se detiene te ahorra pelear a ciegas: cuando entiendas por que no puede inferir, sabras al instante que region falta escribir.
- Predecir los tres fallos clasicos de elision: varias entradas sin
self, salida atada alselfequivocado, y retornos sin fuente unica. - Anotar a mano para sobreescribir una elision correcta pero indeseada, atando la salida al parametro que de verdad la origina.
- Explicar la captura de lifetimes en
impl Traitde retorno y el cambio que introdujo la edicion 2024. - Usar
+ use<...>para capturar con precision solo los parametros que quieres en un retornoimpl Trait.
Los fallos clasicos: cuando ninguna regla decide
Recuerda las tres reglas: cada entrada recibe un lifetime propio; si hay una sola entrada con lifetime, va a la salida; si hay &self, el de self manda sobre las salidas. Los fallos aparecen justo cuando ninguna de las dos ultimas resuelve la salida.
El caso emblematico es varias entradas prestadas y ningun self: la regla 1 reparte un lifetime distinto a cada entrada, pero la salida se queda huerfana y el compilador aborta con E0106 en vez de adivinar.
fn mas_larga(a: &str, b: &str) -> &str { // ERROR E0106
if a.len() >= b.len() { a } else { b }
}
// La cura es tuya: unifica las dos entradas bajo una region comun.
fn mas_larga_ok<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
El segundo fallo es mas insidioso porque compila pero miente: cuando la regla 3 ata la salida a self y tu en realidad querias atarla a un parametro. La firma pasa, pero encadena la vida del resultado al receptor equivocado.
struct Lexer<'s> { fuente: &'s str }
impl<'s> Lexer<'s> {
// Elision: la salida se ata a &self, no a `otro`. Puede no ser lo que quieres.
fn primero(&self, _otro: &str) -> &str { &self.fuente[..1] }
// Si la salida sale de un PARAMETRO, debes anotarlo a mano para romper la regla 3:
fn de_otro<'o>(&self, otro: &'o str) -> &'o str { &otro[..1] }
}
Sobreescribir la elision: anotar no es solo para errores
La leccion anterior te enseno que anotas cuando el compilador no puede inferir. Aqui hay un motivo distinto y mas fino: anotar cuando el compilador infiere algo correcto pero demasiado restrictivo. La elision siempre elige la lectura que compila, no la mas flexible. Si aceptas su eleccion por defecto, a veces atas la salida a una region mas corta de lo necesario y rechazas llamadas legitimas aguas arriba.
// Elision: ambos '_ colapsan a la region de &self; el resultado no puede vivir mas que el struct.
// fn ventana(&self) -> &str
// Anotando, separas la region del dato prestado de la del propio receptor:
struct Buffer<'d> { datos: &'d str }
impl<'d> Buffer<'d> {
// La salida vive tanto como los DATOS ('d), no solo tanto como &self:
fn ventana(&self) -> &'d str { self.datos }
}
La diferencia es real: con la version elidida, el &str devuelto muere cuando muere el prestamo de self; con -> &'d str, sobrevive mientras vivan los datos originales, aunque el Buffer desaparezca. Anotar aqui no arregla un error: amplia el contrato.
La captura de lifetimes en impl Trait: el gran cambio de 2024
El terreno donde la elision se vuelve mas resbaladiza es el retorno impl Trait (RPIT, return position impl Trait). Un tipo impl Iterator oculto puede capturar lifetimes del entorno, y la pregunta “cuales captura” tuvo dos respuestas distintas segun la edicion.
Antes de 2024, un RPIT capturaba los parametros de tipo en scope pero no los lifetimes, salvo que los mencionaras. Devolver un iterador que prestaba de un argumento fallaba con “hidden type captures lifetime that does not appear in bounds”, y el remedio era el ritual + '_ o el truco del trait Captures.
// EDICION 2024: el RPIT captura por defecto TODOS los parametros en scope, tipos y lifetimes.
fn palabras<'a>(texto: &'a str) -> impl Iterator<Item = &'a str> {
texto.split(' ') // captura 'a automaticamente; ya no hace falta el viejo + '_
}
En la edicion 2024 la regla se unifico y simplifico: un RPIT captura todos los parametros genericos en scope, tanto de tipo como de lifetime. Se acabaron los errores de “lifetime no capturado” en el caso normal. Pero el nuevo defecto es mas generoso, y a veces captura de mas: si el tipo oculto no usa realmente cierto lifetime, capturarlo igualmente ata el resultado sin necesidad. Para eso existe la captura precisa con + use<...>, estabilizada en Rust 1.82: enumeras exactamente que parametros quieres capturar.
// El tipo oculto NO usa 'b, pero el defecto de 2024 lo capturaria. use<> lo excluye:
fn solo_a<'a, 'b>(a: &'a str, _b: &'b str) -> impl Iterator<Item = char> + use<'a> {
a.chars()
}
El caso mas cotidiano de este mecanismo es un metodo que devuelve un iterador prestado de self. Antes de 2024 requeria el ritual + '_ para capturar la region del receptor; en 2024 el defecto ya la captura, y solo tocas use<...> si quieres restringirla.
struct Registro { lineas: Vec<String> }
impl Registro {
// Edicion 2024: el impl Iterator captura la region de &self por defecto, sin + '_.
fn no_vacias(&self) -> impl Iterator<Item = &str> {
self.lineas.iter().map(String::as_str).filter(|s| !s.is_empty())
}
}
flowchart TB f[Firma con lifetimes omitidos] --> q1[Varias entradas prestadas y ningun self?] q1 -->|si| e1[E0106: anota y unifica o separa regiones] q1 -->|no| q2[La salida sale de self o de un parametro?] q2 -->|de self| ok[Regla 3: elision correcta] q2 -->|de un parametro| e2[Anota a mano para romper la regla 3] f --> q3[Devuelves impl Trait?] q3 -->|si| cap[Edicion 2024: captura todo en scope; usa use para precisar]
Fuera de las firmas de funciones y metodos, la comodidad desaparece. Los campos de un struct siempre declaran su lifetime a mano. Los items static y const con referencias exigen 'static explicito. En los objetos de trait rige un juego de defectos propio —Box<dyn Trait> significa Box<dyn Trait + 'static>, tema de la leccion 4— que no es la elision de funciones. Y en las firmas con HRTB la region esta cuantificada, no elidida (leccion 3). Saber que la elision es local a las firmas te evita buscar reglas que ahi no aplican.
Merece la pena detenerse en lo que significa que la captura de impl Trait haya cambiado entre ediciones, porque revela algo sobre la naturaleza misma de la elision. Las reglas de elision no son teoremas: son politicas de defecto, decisiones de diseno sobre que lectura asumir cuando el programador calla. Y como toda politica, pueden estar mal calibradas y corregirse. El defecto pre-2024 para RPIT —capturar tipos pero no lifetimes— parecia razonable cuando se diseno, pero la practica demostro que generaba una friccion constante: el programador casi siempre queria capturar el lifetime, y el lenguaje le obligaba a un ritual vacio (+ '_, el truco Captures) para pedir lo que deberia haber sido el defecto. La edicion 2024 invirtio la politica: capturar todo por defecto y ofrecer use<...> para restar lo que sobra. Este es un patron profundo del diseno de lenguajes que trasciende Rust: el buen defecto es el que acierta en el caso mayoritario y deja una valvula explicita para el minoritario, nunca al reves. Y observa la elegancia de la valvula: use<...> no es una anotacion de lifetime mas, es una forma de cuantificar que dependencias existen entre el tipo oculto y su entorno, hecha sintaxis. Cuando escribes + use<'a> estas afirmando un contrato de aislamiento: “este valor depende de ’a y de nada mas”. Que ese contrato sea verificable y estable a traves de una frontera de abstraccion —puedes cambiar el cuerpo de la funcion sin cambiar que captura— es lo que separa una firma robusta de una fragil. La leccion para el ingeniero es doble. Primero: cuando una edicion cambia un defecto, no memorices la sintaxis nueva, entiende que friccion venia a eliminar, porque ahi esta el juicio de diseno que debes internalizar. Segundo: la elision nunca fue “el compilador infiriendo por ti”; fue siempre “el lenguaje eligiendo por ti”, y donde hay una eleccion hay una politica, y donde hay una politica hay un defecto que alguien calibro y que tu puedes anular cuando tu caso no es el comun.
La elision se rinde en tres situaciones: varias entradas prestadas sin self (E0106, unifica o separa regiones a mano), una salida que sale de un parametro cuando la regla 3 la ataria a self (anota para romperla), y retornos sin fuente unica. Anotar tambien sirve para ampliar el contrato: atar la salida a los datos ('d) en vez de al receptor. En impl Trait de retorno, la edicion 2024 captura por defecto todos los parametros en scope —tipos y lifetimes—, resolviendo los viejos errores de captura; usa + use<...> para capturar con precision y excluir lo que sobra. La elision es local a firmas de funciones y metodos: campos, static, objetos de trait y HRTB juegan con otras reglas.
- Escribe
fn mas_larga(a: &str, b: &str) -> &str, lee el E0106 y arreglalo de dos formas: unificando ambas entradas bajo un'ay, alternativamente, atando la salida solo aa. - En un metodo
fn de_otro(&self, otro: &str) -> &strque devuelve parte deotro, comprueba que la elision lo ata alselfequivocado y corrigelo anotando<'o>para atarlo al parametro. - Crea un
Buffer<'d>con un metodo que devuelva&'d stry demuestra que el resultado sobrevive a la caida del&self, algo que la version elidida-> &strno permite. - Escribe
fn palabras<'a>(texto: &'a str) -> impl Iterator<Item = &'a str>en un crate con edicion 2024 y confirma que compila sin ningun+ '_; razona que capturo el defecto. - Toma una funcion con dos lifetimes
'ay'bcuyoimpl Traitde retorno solo use'a, anade+ use<'a>y explica que llamadas aguas arriba pasan a compilar al dejar de capturar'b.