Ir al contenido

Lifetimes en Rust

Los lifetimes (tiempos de vida) suelen ser el primer muro de verdad con el que se topa quien empieza con Rust. Esta página va con calma: el objetivo es solo que reconozcas qué es un lifetime y por qué el compilador te pide uno, no que domines todas sus reglas.

Ya sabes, por la página de borrowing, que una referencia (&) te permite usar un valor sin ser su dueño. Pero una referencia solo es válida mientras el valor al que apunta siga existiendo. Rust tiene que asegurarse de que nunca acabes con una referencia a algo que ya se limpió. Ese tipo de error se llama dangling reference (referencia colgante), y es una fuente clásica de fallos en lenguajes como C.

La mayoría de las veces, Rust lo averigua solo mirando los ámbitos de tu código, y tú no tienes que pensar en ello (es justo lo que viste en la página de ámbito). Pero a veces, sobre todo con funciones, el compilador no puede deducirlo por sí mismo y te pide que lo escribas tú. Eso es una anotación de lifetime.

Esta función devuelve el más largo de dos string slices:

fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}

No compila. El error es este:

error[E0106]: missing lifetime specifier

El compilador dice que falta un especificador de lifetime. El problema es que la referencia que se devuelve podría venir de a o de b, y Rust no puede saber cuál solo leyendo la función. Por eso no puede garantizar que la referencia que recibes siga siendo válida cuando la uses.

Se arregla poniéndole nombre a un lifetime (por convención, una letra minúscula como 'a) y diciéndole a Rust que el lifetime del valor devuelto está ligado a los dos parámetros:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("hi");
let result = longest(&s1, &s2);
println!("La más larga: {result}");
}

Salida:

La más larga: hello
▶ Pruébalo tú mismo

Dejamos "hello" y "hi" en inglés a propósito: la función compara longitudes con .len(), que cuenta bytes, y las letras con tilde ocupan más de un byte.

Lee 'a como una etiqueta, no como un valor: no fija ninguna duración real. Solo le dice a Rust “todas estas referencias tienen que seguir siendo válidas, como mínimo, durante el mismo tramo de código”. Después, el compilador revisa cada llamada para asegurarse de que eso se cumple.

Sin esta anotación, el compilador de verdad no puede saber si la referencia que devuelve longest podría vivir más que alguno de los strings de los que salió. La anotación de lifetime es lo que le permite demostrar (al compilar y sin ningún coste en tiempo de ejecución) que la referencia que recibes siempre será válida.

El siguiente lugar donde te encontrarás lifetimes es en un struct. Hasta ahora, cada campo de un struct era dueño de sus datos, como un String. Un campo también puede ser una referencia, pero entonces el struct solo es válido mientras existan los datos a los que apunta. Rust te obliga a dejarlo por escrito con un lifetime:

struct Highlight<'a> {
text: &'a str, // toma prestado un String que vive en otro lugar
}
fn main() {
let book = String::from("Call me Ishmael. Some years ago...");
let first = Highlight { text: &book[..15] };
println!("Fragmento: {}", first.text);
println!("El libro completo ocupa {} bytes", book.len());
}

Salida:

Fragmento: Call me Ishmael
El libro completo ocupa 34 bytes
▶ Pruébalo tú mismo

Lee Highlight<'a> como “un Highlight que toma prestado algo durante el lifetime 'a”. La parte &book[..15] es un slice (porción): toma prestados los primeros 15 bytes de book sin copiarlos. Como first solo toma prestado, book se puede seguir usando después. (El texto del libro, el comienzo de Moby Dick, se queda en inglés porque cortamos por bytes: con letras con tilde, el corte y la longitud cambiarían.)

El lifetime es lo que permite a Rust detectar este error, en el que el String se libera mientras el struct todavía apunta a él:

struct Highlight<'a> {
text: &'a str,
}
fn main() {
let highlight;
{
let book = String::from("Call me Ishmael. Some years ago...");
highlight = Highlight { text: &book[..15] };
} // aquí se libera book...
println!("{}", highlight.text); // ...pero highlight todavía apunta a él
}
error[E0597]: `book` does not live long enough
--> src/main.rs:9:40
|
8 | let book = String::from("Call me Ishmael. Some years ago...");
| ---- binding `book` declared here
9 | highlight = Highlight { text: &book[..15] };
| ^^^^ borrowed value does not live long enough
10 | } // aquí se libera book...
| - `book` dropped here while still borrowed
11 |
12 | println!("{}", highlight.text); // ...pero highlight todavía apunta a él
| -------------- borrow later used here

El compilador dice que book no vive lo suficiente: se libera al final del bloque (línea 10) mientras sigue prestado, y el préstamo se usa más tarde, en la línea 12.

Un consejo práctico: si los lifetimes de un struct te están complicando la vida, guarda un String propio en lugar de un &str. Así se copia el texto, pero el struct ya no depende de nada más y el lifetime desaparece. Muchos programadores de Rust con experiencia empiezan así y solo cambian a referencias cuando tienen un motivo para hacerlo.

Escribirás 'a muchas menos veces de lo que esta página podría hacerte pensar. Muchísimo código Rust nunca necesita un lifetime explícito, porque el compilador puede deducirlos en la mayoría de las situaciones del día a día. Te los encontrarás sobre todo cuando una función devuelva una referencia y el compilador no pueda averiguar por sí mismo de dónde viene. En ese momento, recuerda: no es un fallo tuyo, es el compilador pidiéndote que confirmes algo que no puede demostrar solo.

Escribe una función shortest que reciba dos string slices y devuelva el más corto. Si tienen la misma longitud, devuelve el primero. Llámala con "elefante" y "gato".

Pista: tiene la misma forma que longest, más arriba. Necesitas <'a> después del nombre, y 'a en los dos parámetros y en el tipo de retorno.

Ver solución
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let big = String::from("elefante");
let small = String::from("gato");
println!("La más corta: {}", shortest(&big, &small));
println!("En caso de empate, gana la primera: {}", shortest("mono", "pato"));
}

Salida:

La más corta: gato
En caso de empate, gana la primera: mono
▶ Pruébalo tú mismo

El slice devuelto puede venir de a o de b, así que Rust necesita 'a para saber que el resultado solo es válido mientras lo sean las dos entradas. Usar <= en lugar de < hace que un empate elija a. (Aquí sí podemos usar palabras en español porque ninguna lleva tildes: .len() cuenta bytes, y cada una de estas letras ocupa exactamente uno.)

Crea un struct Quote con dos campos &str, text y author. Crea uno en el que text tome prestados los primeros 20 bytes del String "Fearless concurrency is a goal of Rust" y author sea el literal "El libro de Rust". Imprímelo como Fearless concurrency (El libro de Rust).

Pista: igual que Highlight más arriba, el struct necesita un lifetime: struct Quote<'a>, con &'a str en los dos campos. &speech[..20] es un slice de los primeros 20 bytes.

Ver solución
struct Quote<'a> {
text: &'a str,
author: &'a str,
}
fn main() {
let speech = String::from("Fearless concurrency is a goal of Rust");
let quote = Quote {
text: &speech[..20],
author: "El libro de Rust",
};
println!("{} ({})", quote.text, quote.author);
}

Salida:

Fearless concurrency (El libro de Rust)
▶ Pruébalo tú mismo

quote toma prestado de speech, así que no puede vivir más que él. El literal "El libro de Rust" va incluido en el propio programa y vive durante toda la ejecución, así que dura de sobra para 'a. Prueba a crear speech dentro de un bloque { } interno e imprimir la cita cuando el bloque ya ha terminado, como en el ejemplo del error de arriba, y verás cómo el compilador te detiene. (La frase, “la concurrencia sin miedo es un objetivo de Rust”, se queda en inglés porque la cortamos por bytes.)

  • Un lifetime le dice al compilador cómo se relacionan las referencias entre sí, para que pueda descartar las referencias colgantes.
  • Solo tienes que escribirlo cuando el compilador no puede deducirlo por sí mismo; lo más habitual es en funciones que devuelven una referencia.
  • 'a no fija una duración: enlaza los lifetimes de distintas referencias para que el compilador pueda comprobarlos.
  • Un struct que guarda una referencia también necesita un lifetime (struct Highlight<'a>), y no puede vivir más que los datos que toma prestados. Guardar un String propio lo evita.
  • Es una comprobación que ocurre solo al compilar; no añade ningún coste en tiempo de ejecución.

Comprueba lo aprendido

1. ¿Qué problema resuelven los lifetimes?

2. ¿Por qué fn longest(a: &str, b: &str) -> &str no compilaba sin una anotación de lifetime?

3. ¿Añadir una anotación de lifetime tiene algún coste en tiempo de ejecución?

4. Según esta página, ¿cuándo necesitas escribir un lifetime explícito con más frecuencia?

Puntuación: 0 / 4