9. Tiempos de vida
Ejemplo completo: examples/l09_lifetimes.rs — cargo run --example l09_lifetimes.
El problema que oculta un recolector de basura
Sección titulada «El problema que oculta un recolector de basura»En C# o en Java, una referencia mantiene vivo su objeto: mientras puedas alcanzarlo, el recolector de basura no lo liberará. Una «referencia colgante» sencillamente no puede existir.
Rust no tiene recolector de basura. Un valor se libera cuando su propietario sale de ámbito (lección 3), así que el compilador debe demostrar que ninguna referencia sigue en uso en ese momento:
let r;{ let s = String::from("hello"); r = &s;}println!("{r}");error[E0597]: `s` does not live long enough --> e09_dangling.rs:5:13 |4 | let s = String::from("hello"); | - binding `s` declared here5 | r = &s; | ^^ borrowed value does not live long enough6 | } | - `s` dropped here while still borrowed7 | println!("{r}"); | - borrow later used hereEl intervalo durante el cual una referencia es válida es su tiempo de vida (lifetime). Dentro de una sola función, el compilador deduce los tiempos de vida por sí mismo — llevas contando con ello desde la lección 4. Solo los escribes cuando una referencia cruza una frontera de función o de struct y el compilador no puede adivinar la relación.
Anotar una función
Sección titulada «Anotar una función»¿De qué entrada toma prestado el resultado?
fn longest(a: &str, b: &str) -> &str { if a.len() >= b.len() { a } else { b }}error[E0106]: missing lifetime specifier --> e09_longest.rs:1:33 |1 | fn longest(a: &str, b: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`help: consider introducing a named lifetime parameter |1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { | ++++ ++ ++ ++El compilador comprueba cada función solo por su firma, nunca por su cuerpo — igual que quien llama a un método en C# solo ve su declaración. Así que la firma tiene que decirlo:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() >= b.len() { a } else { b }}Lee <'a> como un parámetro genérico (se declara en el mismo lugar que <T>): «para algún tiempo de vida 'a durante el cual a y b son válidos, el resultado también es válido durante 'a». En la práctica, 'a pasa a ser el más corto de los dos, de modo que quien llama no puede conservar el resultado más tiempo que cualquiera de las entradas:
let title = String::from("Rust for C# developers");let winner;{ let subtitle = String::from("ownership"); winner = longest(&title, &subtitle);}println!("{winner}");error[E0597]: `subtitle` does not live long enough --> e09_longest_scope.rs:10:34 | 9 | let subtitle = String::from("ownership"); | -------- binding `subtitle` declared here10 | winner = longest(&title, &subtitle); | ^^^^^^^^^ borrowed value does not live long enough11 | } | - `subtitle` dropped here while still borrowed12 | println!("{winner}"); | ------ borrow later used hereEn tiempo de ejecución, title es la cadena más larga, así que esto funcionaría por casualidad — pero la firma no promete nada sobre cuál se devuelve, y el compilador te obliga a atenerte a la firma.
Anota solo lo que el resultado toma prestado de verdad. Aquí el resultado viene únicamente de a, así que len_from no necesita tiempo de vida y puede ser cualquier cosa efímera:
fn prefix_of<'a>(a: &'a str, len_from: &str) -> &'a str { &a[..len_from.len().min(a.len())]}Elisión: cuándo puedes omitirlos
Sección titulada «Elisión: cuándo puedes omitirlos»La mayoría de las funciones nunca mencionan un tiempo de vida, porque tres reglas de elisión los completan:
- Cada parámetro referencia recibe su propio tiempo de vida.
- Si hay exactamente un tiempo de vida de entrada, se usa para todas las referencias de salida.
- Si uno de los parámetros es
&selfo&mut self, se usa su tiempo de vida para las salidas.
fn first_word(text: &str) -> &str { // regla 2: el resultado toma prestado `text` text.split_whitespace().next().unwrap_or("")}longest necesitaba una anotación porque tiene dos entradas y ningún self: no se aplica ninguna regla.
La elisión nunca hace compilar un programa incorrecto. Cuando las reglas producen un tiempo de vida que no encaja, sigues obteniendo un error — y devolver una referencia a una variable local es siempre un error, anotes lo que anotes:
fn shout(word: &str) -> &str { let upper = word.to_uppercase(); &upper}error[E0515]: cannot return reference to local variable `upper` --> e09_local.rs:3:5 |3 | &upper | ^^^^^^ returns a reference to data owned by the current functionLa solución es la de la lección 4: devolver el String con propietario.
Structs que toman prestado
Sección titulada «Structs que toman prestado»Un struct que contiene una referencia debe declarar el tiempo de vida, igual que un parámetro de tipo genérico:
struct Excerpt { text: &str,}error[E0106]: missing lifetime specifier --> e09_struct.rs:2:11 |2 | text: &str, | ^ expected named lifetime parameter |help: consider introducing a named lifetime parameter |1 ~ struct Excerpt<'a> {2 ~ text: &'a str, |struct Excerpt<'a> { text: &'a str,}
impl Excerpt<'_> { // '_ : «algún tiempo de vida, no necesito su nombre» fn word_count(&self) -> usize { self.text.split_whitespace().count() }}
let novel = String::from("Call me Ishmael. Some years ago, never mind how long precisely...");let excerpt = Excerpt { text: novel.split('.').next().unwrap_or("") };// excerpt: "Call me Ishmael" (3 words)Excerpt<'a> se lee «un extracto que no puede sobrevivir al texto al que apunta». El compilador lo hace cumplir:
error[E0597]: `novel` does not live long enough --> e09_struct_outlive.rs:9:35 | 8 | let novel = String::from("Call me Ishmael. Some years ago..."); | ----- binding `novel` declared here 9 | excerpt = Excerpt { text: novel.split('.').next().unwrap() }; | ^^^^^ borrowed value does not live long enough10 | } | - `novel` dropped here while still borrowed11 | println!("{}", excerpt.text); | ------------ borrow later used hereEl equivalente más cercano en C# es un ref struct como Span<T>: puede apuntar a la memoria de otro, así que el compilador restringe adónde puede ir. En Rust, cualquier struct puede ser así.
El tiempo de vida en la firma de un método importa
Sección titulada «El tiempo de vida en la firma de un método importa»Un tokenizador que devuelve slices de su entrada:
struct Parser<'a> { input: &'a str, pos: usize,}
impl<'a> Parser<'a> { fn next_token(&mut self) -> Option<&'a str> { // … devuelve &self.input[start..end] }}
fn tokenize(line: &str) -> Vec<&str> { let mut parser = Parser::new(line); let mut tokens = Vec::new(); while let Some(token) = parser.next_token() { tokens.push(token); } tokens // el parser se libera, los tokens sobreviven}// tokens: ["let", "x", "=", "42"]El tipo de retorno dice &'a str: un token toma prestado el texto de entrada, no el parser. Si se hubiera escrito Option<&str>, la regla de elisión 3 ataría cada token a &mut self — y no podrías conservar dos tokens a la vez (ejercicio 3).
'static
Sección titulada «'static»'static es el tiempo de vida de los datos válidos durante toda la ejecución del programa. Los literales de cadena se almacenan en el binario, así que lo tienen:
fn default_greeting() -> &'static str { "hello"}Te encontrarás 'static sobre todo como restricción (bound), por ejemplo en std::thread::spawn: un hilo nuevo puede sobrevivir a la función que lo inició, así que no puede tomar prestadas las variables locales de esa función.
let name = String::from("worker");let label: &str = &name;let handle = std::thread::spawn(move || println!("{label}"));handle.join().unwrap();error[E0597]: `name` does not live long enough --> e09_static.rs:4:23 | 3 | let name = String::from("worker"); | ---- binding `name` declared here 4 | let label: &str = &name; | ^^^^^ borrowed value does not live long enough 5 | let h = thread::spawn(move || println!("{label}")); | ------------------------------------------ argument requires that `name` is borrowed for `'static` 6 | h.join().unwrap(); 7 | } | - `name` dropped here while still borrowed |note: requirement that the value outlives `'static` introduced hereMover el propio String a la closure lo arregla. T: 'static no significa «vive para siempre»: significa «no contiene datos prestados que puedan caducar» — un String con propietario cumple la condición. Los hilos son el tema de la lección 12.
Cuando los tiempos de vida estorban: sé dueño de los datos
Sección titulada «Cuando los tiempos de vida estorban: sé dueño de los datos»Viniendo de C#, el reflejo es guardar referencias en todas partes. En Rust, un struct lleno de campos &'a contagia su tiempo de vida a todo lo que lo contiene. Una regla práctica mientras aprendes:
| Situación | Usa |
|---|---|
| Parámetros de función que solo lees | &str, &[T], &T |
| Vistas efímeras sobre datos que posee otro (parsers, iteradores, extractos) | un struct con 'a |
| Datos que un struct conserva mucho tiempo | String, Vec<T>, T con propietario |
| Datos compartidos por varios propietarios | Rc/Arc — lección 11 |
Clonar unas cuantas cadenas para evitar un parámetro de tiempo de vida es un compromiso perfectamente razonable.
Puntos clave
Sección titulada «Puntos clave»- Un tiempo de vida es el intervalo durante el cual una referencia es válida; el compilador comprueba que ninguna referencia sobreviva a su valor.
- Las anotaciones describen relaciones entre referencias en una firma; nunca alargan la vida de un valor.
- Tres reglas de elisión cubren la mayoría de las funciones; anotas cuando hay varias entradas y ningún
self. - Un struct que contiene una referencia lleva un parámetro de tiempo de vida y no puede sobrevivir a lo que toma prestado.
- Los datos
'staticno contienen préstamos que caduquen; los exigen API comothread::spawn. - En caso de duda, sé dueño de los datos.
Ejercicios
Sección titulada «Ejercicios»- Escribe
fn longest_line(text: &str) -> &str, que devuelve la línea más larga de un texto. ¿Necesita una anotación de tiempo de vida? Después escribefn pick(first: &str, second: &str, use_first: bool) -> &str— ¿qué necesita?
Solución
fn longest_line(text: &str) -> &str { text.lines().max_by_key(|line| line.len()).unwrap_or("")}
fn pick<'a>(first: &'a str, second: &'a str, use_first: bool) -> &'a str { if use_first { first } else { second }}
let poem = String::from("short\na much longer line\nmid");assert_eq!(longest_line(&poem), "a much longer line");assert_eq!(pick("left", "right", false), "right");longest_line tiene una sola referencia de entrada, así que se aplica la regla de elisión 2. pick tiene dos y puede devolver cualquiera de ellas, así que ambas deben compartir 'a — exactamente como longest.
- Define
struct Highlight<'a> { line: &'a str, column: usize }y escribefind_highlights(text, word), que devuelve cada línea detextque contieneword. Quien llama debe poder buscar con una consultaStringtemporal que se libera antes de usar los resultados.
Solución
#[derive(Debug, PartialEq)]struct Highlight<'a> { line: &'a str, column: usize,}
fn find_highlights<'a>(text: &'a str, word: &str) -> Vec<Highlight<'a>> { text.lines() .filter_map(|line| line.find(word).map(|column| Highlight { line, column })) .collect()}
let text = String::from("I like Rust\nC# too\nRust again");let hits = { let query = String::from("Rust"); // se libera al final de este bloque find_highlights(&text, &query)};assert_eq!(hits, [Highlight { line: "I like Rust", column: 7 }, Highlight { line: "Rust again", column: 0 }]);Los resultados solo toman prestado text, así que word recibe su propio tiempo de vida (elidido). Escribir word: &'a str haría compilar la función, pero rechazaría esta llamada con E0597: el compilador supondría entonces que los resultados podrían apuntar a query.
- Esta versión del tokenizador compila, pero
mainno. Explica el error y corrígelo cambiando una sola línea.
impl<'a> Parser<'a> { fn next_token(&mut self) -> Option<&str> { let start = self.pos; self.pos = self.input.len(); Some(&self.input[start..]) }}
let line = String::from("let x = 42");let mut parser = Parser { input: &line, pos: 0 };let first = parser.next_token();let second = parser.next_token();println!("{first:?} {second:?}");error[E0499]: cannot borrow `parser` as mutable more than once at a time --> e09_elided_self.rs:18:18 |17 | let first = parser.next_token(); | ------ first mutable borrow occurs here18 | let second = parser.next_token(); | ^^^^^^ second mutable borrow occurs here19 | println!("{first:?} {second:?}"); | ----- first borrow later used hereSolución
Con Option<&str>, la regla de elisión 3 da al resultado el tiempo de vida de &mut self. Mientras first siga vivo, parser permanece prestado de forma mutable, así que la segunda llamada se rechaza. El token apunta en realidad a la entrada, así que dilo:
fn next_token(&mut self) -> Option<&'a str> {Ahora los tokens toman prestado line, y el parser vuelve a quedar libre en cuanto termina cada llamada.