Ir al contenido

9. Tiempos de vida

Ejemplo completo: examples/l09_lifetimes.rscargo 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 here
5 | r = &s;
| ^^ borrowed value does not live long enough
6 | }
| - `s` dropped here while still borrowed
7 | println!("{r}");
| - borrow later used here

El 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.

¿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 here
10 | winner = longest(&title, &subtitle);
| ^^^^^^^^^ borrowed value does not live long enough
11 | }
| - `subtitle` dropped here while still borrowed
12 | println!("{winner}");
| ------ borrow later used here

En 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())]
}

La mayoría de las funciones nunca mencionan un tiempo de vida, porque tres reglas de elisión los completan:

  1. Cada parámetro referencia recibe su propio tiempo de vida.
  2. Si hay exactamente un tiempo de vida de entrada, se usa para todas las referencias de salida.
  3. Si uno de los parámetros es &self o &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 function

La solución es la de la lección 4: devolver el String con propietario.

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 enough
10 | }
| - `novel` dropped here while still borrowed
11 | println!("{}", excerpt.text);
| ------------ borrow later used here

El 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 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 here

Mover 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/Arclección 11

Clonar unas cuantas cadenas para evitar un parámetro de tiempo de vida es un compromiso perfectamente razonable.

  • 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 'static no contienen préstamos que caduquen; los exigen API como thread::spawn.
  • En caso de duda, sé dueño de los datos.
  1. 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 escribe fn 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.

  1. Define struct Highlight<'a> { line: &'a str, column: usize } y escribe find_highlights(text, word), que devuelve cada línea de text que contiene word. Quien llama debe poder buscar con una consulta String temporal 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.

  1. Esta versión del tokenizador compila, pero main no. 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 here
18 | let second = parser.next_token();
| ^^^^^^ second mutable borrow occurs here
19 | println!("{first:?} {second:?}");
| ----- first borrow later used here
Solució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.