Anotar lifetimes en funciones: atar la salida a la entrada
Cuándo hace falta escribir `fn foo<'a>(x: &'a str) -> &'a str` a mano: relacionar la vida de una referencia de salida con la de una de entrada cuando el compilador no puede adivinar de cuál sale. Qué error resuelve E0106 y por qué la anotación es un contrato, no una orden.
Una función que recibe referencias y devuelve una referencia plantea una pregunta que el compilador no siempre puede responder solo: la referencia que sale, ¿de cuál de las que entran proviene? Cuando hay ambigüedad, tú se lo dices con una anotación de lifetime. fn mas_larga<'a>(a: &'a str, b: &'a str) -> &'a str no manda que nada viva más: ata la región de la salida a la de las entradas, para que el checker verifique a la vez el cuerpo de la función y cada punto donde la llamas.
- Leer una anotación como
<'a>como la declaración de una región genérica. - Saber cuándo hace falta: relacionar el lifetime de salida con uno de entrada.
- Diagnosticar E0106 “missing lifetime specifier” y por qué aparece.
- Entender que la anotación es un contrato que se verifica en la llamada.
El caso que obliga a anotar
Considera una función que devuelve la más larga de dos cadenas prestadas. La referencia de salida es una de las dos de entrada, pero el compilador —que analiza cada firma de forma aislada, sin mirar el cuerpo— no puede saber cuál. Sin una pista, se planta:
fn mas_larga(a: &str, b: &str) -> &str { // ERROR E0106: missing lifetime specifier
if a.len() >= b.len() { a } else { b }
}
El mensaje es “missing lifetime specifier”: el compilador te dice que la salida &str podría salir de a o de b, cuyas regiones pueden ser distintas, y necesita que ates la salida a una de ellas para poder garantizar que la referencia devuelta no sobreviva a su dato. La cura es introducir un lifetime genérico 'a y usarlo para expresar la relación:
fn mas_larga<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b } // la salida vive lo que la MENOR de las entradas
}
¿Por qué el compilador no mira el cuerpo y deduce solo que la salida puede salir de a o de b? Porque analiza cada firma de forma aislada, sin abrir la implementación. Es lo que permite compilar módulos por separado y cambiar un cuerpo sin re-verificar a quien lo llama: la firma es el único contrato visible desde fuera, así que tiene que bastarse por sí sola para garantizar la seguridad temporal. Por eso un lifetime que falta en la firma no es un olvido menor, sino un contrato incompleto que el compilador se niega a aceptar.
Qué dice exactamente <'a>
El <'a> tras el nombre declara un parámetro de lifetime, igual que <T> declara un parámetro de tipo. No nombra una región fija: es una región genérica que el llamador instanciará en cada uso. Al escribir 'a en a, en b y en la salida, estás afirmando una relación: “existe una región 'a tal que ambas entradas viven al menos 'a, y la salida vive exactamente 'a”.
'a es genérico
Como T, el lifetime 'a lo elige quien llama, no la función. Cada llamada lo instancia con una región concreta distinta.
'a ata regiones
Repetir 'a en entrada y salida declara que comparten región: la salida no puede vivir más que la entrada que la origina.
En el punto de llamada, el compilador instancia 'a con la intersección de las regiones reales de los argumentos: la mayor región en la que ambos siguen vivos. La referencia devuelta hereda esa región, y por tanto no puede usarse después de que muera el más efímero de los dos datos.
fn main() {
let largo = String::from("una cadena bien larga");
let elegida;
{
let corto = String::from("breve");
elegida = mas_larga(&largo, &corto); // 'a = intersección: dura lo que `corto`
println!("{elegida}"); // OK: `corto` aún vive aquí
} // muere `corto`, y con él la región 'a
// println!("{elegida}"); // ERROR: 'a ya terminó
}
La anotación es un contrato, no una orden
Este es el malentendido que hay que erradicar: 'a no hace que nada viva más tiempo. Es una restricción que el compilador comprueba en dos frentes. Dentro de la función, verifica que el cuerpo respeta lo prometido (que solo devuelves referencias que de verdad viven 'a). Fuera, en cada llamada, verifica que los argumentos reales satisfacen la relación y propaga la región resultante a la salida. La anotación describe una verdad que el checker demuestra; no ordena un comportamiento en ejecución.
// El compilador verifica el CUERPO contra la promesa de la firma:
fn intruso<'a>(x: &'a str) -> &'a str {
let local = String::from("nace y muere aquí");
// &local // ERROR E0515: la firma promete 'a, pero `local` no vive 'a
x // única salida honesta: la entrada, que sí vive 'a
}
Prometer 'a en la salida no te autoriza a devolver lo que quieras: el checker exige que lo devuelto viva de verdad 'a. Devolver &local rompe el contrato desde dentro, y el error salta en el cuerpo, no en la llamada. Así se cierra el círculo: la firma es una promesa, y el cuerpo tiene que cumplirla antes de que ningún llamador confíe en ella.
Poner el mismo 'a a dos parámetros no siempre es correcto: los estás obligando a compartir región, y a veces mientes. Si la salida solo puede salir de a, dilo con precisión y deja b con su propia región. Sobre-atar lifetimes es una fuente silenciosa de errores en las llamadas: la firma compila, pero rechaza usos que deberían valer.
// La salida SOLO proviene de `texto`; `patron` es independiente.
fn antes_de<'a, 'b>(texto: &'a str, patron: &'b str) -> &'a str {
match texto.find(patron) {
Some(i) => &texto[..i], // la salida es una porción de `texto`: vive 'a
None => texto,
}
} // `patron` puede morir en cuanto vuelve la función: no ata la salida
Aquí 'b no aparece en el retorno, así que la región de patron no restringe la de la salida. Declarar dos lifetimes distintos es más permisivo: acepta que patron sea mucho más efímero que texto. Elegir cuántos lifetimes y cómo relacionarlos es diseñar el contrato temporal de la función.
Cuándo NO hace falta anotar
No toda función que toma referencias necesita anotaciones. Si no devuelves ninguna referencia, o si la elisión (lección 4) puede inferir la relación, no escribes nada. La anotación explícita solo es obligatoria cuando hay ambigüedad genuina sobre de qué entrada sale una referencia de salida: varias entradas prestadas candidatas y ninguna regla que decida por ti.
Un caso especialmente cómodo: tomar referencias pero devolver un valor por propiedad. Como la salida no es una referencia, no hay región que atar y no anotas nada, por muchas entradas prestadas que haya.
// Dos entradas prestadas, pero la salida es dueña: cero lifetimes.
fn concatena(a: &str, b: &str) -> String {
format!("{a}{b}") // devuelve un String nuevo, sin ninguna referencia que atar
}
flowchart TB q[Devuelves una referencia?] -->|no| nada[No anotas nada] q -->|si| c[Hay una sola entrada prestada?] c -->|si| elide[La elision la infiere sola] c -->|varias entradas| amb[Ambiguo: de cual sale?] amb --> anota[Anotas un lifetime para atar salida y entrada]
// Una sola entrada prestada: sin ambigüedad, sin anotación.
fn primera_palabra(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
} // el compilador ata la salida a la única entrada por elisión
Ante una firma que devuelve una referencia, hazte siempre la misma pregunta antes de escribir un solo 'a: ¿de qué dato de entrada procede esta salida? Si la respuesta es “de una sola, sin duda”, no anotes: la elisión lo resuelve. Si es “de varias, o depende”, tienes que anotar y decidir la relación. Y si es “de ninguna, la creo dentro”, entonces no devuelvas una referencia en absoluto: devuelve el valor por propiedad. Casi todos los enredos con lifetimes en funciones se disuelven contestando con honestidad esa única pregunta sobre el origen de cada referencia devuelta.
La lección profunda es que una firma en Rust no describe solo qué tipos entran y salen, sino qué regiones los relacionan. fn mas_larga<'a>(a: &'a str, b: &'a str) -> &'a str es un contrato con dos cláusulas que el compilador hace cumplir a ambas partes. La función promete: “si me das dos referencias que viven al menos 'a, te devuelvo una que vive 'a, ni un punto más”. El llamador se compromete: “los datos que presto seguirán vivos durante toda 'a, así que no usaré el resultado después de que muera el más efímero”. Ninguna de las dos partes puede engañar a la otra, porque el checker verifica el cuerpo contra la promesa y cada llamada contra el compromiso. Esto es composicionalidad: puedes razonar sobre la función mirando solo su firma, sin abrir su cuerpo, y encadenar mil llamadas con la garantía de que ninguna referencia colgará en ningún eslabón. Los lenguajes con punteros crudos no tienen este contrato: en C, que una función devuelva un puntero válido depende de una convención escrita en un comentario que nadie verifica. Rust sube esa convención al sistema de tipos y la convierte en una obligación demostrada. Por eso escribir 'a no es ceder a un compilador quisquilloso: es documentar, en un lenguaje que el compilador entiende, la relación temporal exacta que tu función respeta.
Anotas un lifetime cuando devuelves una referencia y hay varias entradas prestadas candidatas: el <'a> declara una región genérica y repetirla ata la salida a la entrada que la origina. La anotación es un contrato, no una orden: no alarga la vida de nada, sino que el checker la verifica en el cuerpo y en cada llamada, donde 'a se instancia con la intersección de las regiones reales. Si solo hay una entrada prestada, o no devuelves referencias, la elisión te ahorra escribirla. Y no ates con el mismo 'a parámetros que en realidad son independientes.
- Escribe
mas_largasin lifetimes, lee el E0106 completo, y arréglala; luego prueba a llamarla con un argumento en un bloque interno y explica el fallo. - Escribe
antes_de<'a, 'b>y comprueba que unpatronmuy efímero se acepta; luego únelo con'aatextoy observa qué llamadas dejan de compilar. - Justifica por qué
primera_palabrano necesita anotación peromas_largasí. - Explica en una frase por qué la anotación es un contrato y no una instrucción en ejecución.
- Diseña la firma de una función que reciba dos slices y devuelva uno de ellos, decidiendo con cuidado qué lifetimes comparten y cuáles no.