Lifetimes
Los lifetimes nunca hacen que algo viva más tiempo: son la forma de describirle al compilador una relación entre referencias que ya existía en tu código.
Hasta ahora, el borrow checker ha averiguado en silencio cuánto tiempo necesita vivir cada referencia con solo mirar dónde se crea y dónde se usa por última vez. Esa inferencia se viene abajo en el momento en que una referencia podría venir, de forma plausible, de más de un lugar. Piensa en una función que recibe dos slices de string y devuelve el que sea más largo: el valor de retorno es una referencia, pero ¿una referencia a qué? El compilador no puede mirar dentro del cuerpo de la función, ver un if y saber de antemano qué rama se ejecutará para una llamada dada, así que no puede deducir por sí solo si la referencia devuelta debería atarse al tiempo de vida del primer argumento o al del segundo.
Esta es la única situación en la que tienes que ayudar al compilador escribiendo una anotación de lifetime: un nombre como 'a (se lee 'tick-a') que etiqueta un tiempo de vida para que pueda aparecer más de una vez en una firma. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str declara un parámetro de lifetime genérico 'a y luego lo reutiliza en ambos parámetros y en el tipo de retorno. Esto no crea un lifetime, no extiende ninguno ni hace que nada viva más de lo que viviría de todas formas: es una restricción que la firma de la función le promete a quien la llame: 'la referencia que te devuelvo es válida exactamente durante el tiempo de vida más corto de las dos referencias que me diste.'
El compilador hace cumplir esa promesa en cada llamada. Si llamas a longest con un string que sale de alcance antes que el otro, el compilador no te dejará usar el resultado después de ese punto — no porque vuelva a analizar el cuerpo de la función en cada sitio de llamada, sino porque la firma ya le contó la regla, y comprobar a quien llama contra una regla es exactamente para lo que está construido un borrow checker. Esto también explica por qué la anotación no te sirve de nada si la escribes mal: poner 'a no hace que el código sea correcto, hace una afirmación, y el compilador verifica por su cuenta que el cuerpo de la función realmente cumpla esa afirmación.
Si todas las funciones necesitaran anotaciones de lifetime explícitas, escribir Rust sería insoportable, así que el compilador aplica primero tres reglas de elisión y solo te pide que las escribas cuando esas reglas no resuelven la ambigüedad. Primero, cada parámetro de referencia recibe su propio lifetime implícito. Segundo, si hay exactamente un lifetime de entrada, se asigna a todo lifetime de salida elidido. Tercero, si uno de los parámetros es &self o &mut self, su lifetime se asigna a toda salida elidida — por eso los métodos que devuelven una referencia derivada de self casi nunca necesitan anotaciones. longest necesitó una precisamente porque tiene dos referencias de entrada y ningún &self que desempate.
Los structs también pueden guardar referencias, y cuando lo hacen, la propia definición del struct necesita un parámetro de lifetime: struct Excerpt<'a> { part: &'a str } dice que un Excerpt no puede vivir más que el slice de string del que su campo part toma prestado. Esto es el compilador protegiéndote de un struct que de otro modo quedaría colgando: si el String original al que apunta part se destruyera mientras todavía existe un Excerpt que lo referencia, tendrías un use-after-free, exactamente la clase de bug que Rust existe para eliminar en tiempo de compilación en vez de a las 3 de la mañana en producción.
La frase más útil que puedes interiorizar sobre los lifetimes es esta: no cambian cuánto tiempo vive nada. Una anotación de lifetime no es una orden para que el compilador mantenga un valor vivo más tiempo, como podría hacer el análisis de alcanzabilidad de un recolector de basura; es la descripción de una relación —'la validez de esta referencia depende de la de aquella'— que ya existe en tu código, la escribas o no. Nombrarla solo le da al borrow checker la información suficiente para verificar una relación que cruza el límite de una función, un punto donde de otro modo no puede ver lo bastante lejos como para deducirla por sí mismo.
Por eso, pelear contra un error de lifetime salpicando 'static por todas partes o añadiendo parámetros de lifetime hasta que compile casi siempre empeora las cosas en lugar de mejorarlas: le estás mintiendo al compilador sobre una relación que en realidad no se cumple, y o bien seguirá sin compilar, o compilará mientras obliga en silencio a que ciertos valores vivan más tiempo (y se clonen o se filtren más) de lo necesario. Cuando el borrow checker rechaza una función, la solución casi siempre es reestructurar la propiedad —devolver un String propio en vez de un &str prestado, o cambiar qué valor es dueño de cuál— en lugar de seguir añadiendo anotaciones hasta que el compilador se rinda.
// Without a lifetime annotation, this wouldn't compile://// fn longest(x: &str, y: &str) -> &str {// if x.len() > y.len() { x } else { y }// }//// The compiler can't tell whether the returned reference is tied to// the lifetime of `x` or of `y`, so it refuses to guess.fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}}fn main() {let string1 = String::from("long string is long");let result;{let string2 = String::from("xyz");result = longest(string1.as_str(), string2.as_str());println!("The longest string is {}", result);}}
A function that returns a reference derived from one of two inputs needs an explicit lifetime because the compiler can't tell from the body alone which argument the result is tied to.
// Elided: the compiler fills in the lifetime using elision rule #2 —// exactly one input lifetime, so it's assigned to the output too.fn first_word(s: &str) -> &str {match s.find(' ') {Some(pos) => &s[..pos],None => s,}}// The exact same signature, written out by hand.fn first_word_explicit<'a>(s: &'a str) -> &'a str {match s.find(' ') {Some(pos) => &s[..pos],None => s,}}fn main() {let sentence = String::from("hello world");println!("{}", first_word(&sentence));println!("{}", first_word_explicit(&sentence));}
The same signature written twice: once relying on lifetime elision, once fully spelled out, to show exactly what the elision rules are filling in for you.
struct Excerpt<'a> {part: &'a str,}impl<'a> Excerpt<'a> {// Elision rule #3: a `&self` parameter lends its lifetime to the// return value, so no annotation is needed here even though this// method returns a reference.fn announce(&self, announcement: &str) -> &str {println!("Attention please: {}", announcement);self.part}}fn main() {let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().expect("no '.' found");let excerpt = Excerpt {part: first_sentence,};println!("Excerpt: {}", excerpt.announce("New chapter"));}
A struct holding a borrowed reference must carry a lifetime parameter, and its method can still rely on elision because a &self parameter is present.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. What does adding the lifetime annotation 'a to fn longest<'a>(x: &'a str, y: &'a str) -> &'a str actually do?