En esta página
Lifetimes: anotando la vida de las referencias
¿Por qué existen las lifetimes?
Las lifetimes (tiempos de vida) son la manera en que Rust rastrea cuánto tiempo viven las referencias. No son un concepto que existe en tiempo de ejecución — son puramente información para el compilador.
El borrow checker usa lifetimes para garantizar que ninguna referencia viva más que el dato al que apunta. Sin esta verificación, podríamos tener punteros colgantes.
El problema que resuelven
// ¿Cuál es el lifetime del valor de retorno?
fn mas_largo(x: &str, y: &str) -> &str {
if x.len() >= y.len() { x } else { y }
}Esta función no compila porque el compilador no sabe si el valor de retorno está relacionado con x o con y. Si x y y tienen lifetimes distintos, el valor de retorno podría ser una referencia a algo que ya se liberó.
Sintaxis de anotaciones de lifetime
Las anotaciones de lifetime comienzan con ' seguido de un nombre (típicamente una sola letra minúscula):
&i32 // una referencia
&'a i32 // una referencia con lifetime 'a explícito
&'a mut i32 // una referencia mutable con lifetime 'a explícitoLas anotaciones no cambian cuánto viven las referencias — solo describen la relación entre los lifetimes de múltiples referencias.
Lifetimes en funciones
// 'a es el lifetime más corto entre x e y
// El valor de retorno vive al menos 'a
fn mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
fn main() {
let cadena1 = String::from("cadena larga es larga");
// Caso 1: Ambas viven el mismo tiempo
let cadena2 = String::from("xyz");
let resultado = mas_largo(cadena1.as_str(), cadena2.as_str());
println!("La más larga: {resultado}");
// Caso 2: Una vive menos que la otra
let resultado2;
{
let cadena3 = String::from("otra cadena");
// resultado2 = mas_largo(&cadena1, &cadena3);
// println!("{resultado2}"); // Error si cadena3 ya se dropó
// Correcto: usar el resultado dentro del scope de cadena3
let r = mas_largo(&cadena1, &cadena3);
println!("Resultado en scope: {r}");
}
}Reglas de elision de lifetimes
En la práctica, no siempre necesitas escribir lifetimes manualmente. El compilador aplica tres reglas de elision (elision = omisión) para inferirlas automáticamente:
Regla 1: Cada parámetro de referencia recibe su propio lifetime.
// Lo que escribes:
fn primera(s: &str) -> &str
// Lo que el compilador infiere:
fn primera<'a>(s: &'a str) -> &'a strRegla 2: Si hay exactamente un parámetro de referencia de entrada, su lifetime se asigna a todos los outputs de referencia.
// Lo que escribes:
fn primera_palabra(s: &str) -> &str
// Lo que infiere el compilador:
fn primera_palabra<'a>(s: &'a str) -> &'a strRegla 3: Si uno de los parámetros es &self o &mut self, su lifetime se asigna a todos los outputs de referencia.
struct Texto { contenido: String }
impl Texto {
// Lo que escribes:
fn obtener_slice(&self) -> &str
// Lo que infiere el compilador:
// fn obtener_slice<'a>(&'a self) -> &'a str
}Structs con lifetimes
Cuando un struct contiene referencias, necesita anotaciones de lifetime:
// El struct no puede outlive la referencia que contiene
struct Fragmento<'a> {
texto: &'a str,
inicio: usize,
fin: usize,
}
impl<'a> Fragmento<'a> {
fn nuevo(texto: &'a str, inicio: usize, fin: usize) -> Self {
Fragmento { texto, inicio, fin }
}
fn contenido(&self) -> &str {
&self.texto[self.inicio..self.fin]
}
fn longitud(&self) -> usize {
self.fin - self.inicio
}
}
fn main() {
let texto = String::from("Rust es un lenguaje de sistemas moderno");
let fragmento = Fragmento::nuevo(&texto, 0, 4);
println!("Fragmento: '{}'", fragmento.contenido()); // "Rust"
println!("Longitud: {}", fragmento.longitud());
// Fragmento no puede outlive texto
// drop(texto); // Error si usamos fragmento después
println!("Texto completo: {texto}");
}Lifetime bounds: restricciones de lifetime
Puedes añadir restricciones de lifetime a tipos genéricos:
use std::fmt::Display;
// T debe vivir al menos 'a
fn mostrar_referencia<'a, T>(valor: &'a T)
where
T: Display + 'a,
{
println!("Valor: {valor}");
}
// Una referencia a T debe vivir 'a, y T debe implementar Display
fn encontrar_mas_largo<'a, T>(
lista: &'a [T],
predicado: impl Fn(&T) -> bool,
) -> Option<&'a T>
where
T: Display,
{
lista.iter().find(|item| predicado(item))
}
fn main() {
let numeros = vec![1, 5, 2, 8, 3, 7];
if let Some(encontrado) = encontrar_mas_largo(&numeros, |&n| n > 5) {
println!("Encontrado: {encontrado}");
}
let palabras = vec!["hola", "mundo", "rust"];
mostrar_referencia(&palabras[0]);
}La lifetime `'static`
'static es una lifetime especial que significa "vive por toda la duración del programa":
fn main() {
// String literals son 'static — viven en el binario
let saludo: &'static str = "Hola, mundo";
// Las constantes también son 'static
static CONSTANTE: &str = "soy estática";
println!("{saludo}");
println!("{CONSTANTE}");
}
// Esta función puede retornar una referencia 'static
fn obtener_mensaje(codigo: u32) -> &'static str {
match codigo {
200 => "OK",
404 => "No encontrado",
500 => "Error interno",
_ => "Código desconocido",
}
}'static también aparece en los bounds de trait para objetos trait: Box<dyn Error + 'static> o Box<dyn Error + Send + 'static>. Esto indica que el tipo de error no contiene referencias con lifetimes menores a 'static.
Lifetimes en la práctica
En código Rust real, raramente necesitas escribir lifetimes manualmente porque:
- Las reglas de elision cubren los casos más comunes
- Los structs que poseen sus datos (usando
Stringen lugar de&str,Vecen lugar de&[]) no necesitan anotaciones
// Sin lifetimes — posee sus datos
struct Usuario {
nombre: String, // String, no &str
edad: u32,
}
// Con lifetimes — referencia a datos externos
struct VistaUsuario<'a> {
nombre: &'a str, // &str, necesita lifetime
edad: u32,
}La regla general: prefiere tipos que poseen sus datos en los structs. Usa referencias solo cuando el rendimiento lo requiera y estés dispuesto a gestionar los lifetimes.
Con ownership, borrowing y lifetimes comprendidos, tienes el conocimiento del sistema de tipos único de Rust. La próxima lección introduce los structs — la forma principal de crear tipos de datos personalizados.
// Sin anotación: el compilador no sabe cuánto vive el retorno
// fn mas_largo(x: &str, y: &str) -> &str { ... } // Error
// Con anotación de lifetime 'a
fn mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
// Struct que contiene una referencia necesita lifetime
struct Extracto<'a> {
parte: &'a str,
}
impl<'a> Extracto<'a> {
fn mostrar(&self) {
println!("Extracto: {}", self.parte);
}
}
fn main() {
let cadena1 = String::from("cadena larga");
let resultado;
{
let cadena2 = String::from("xyz");
resultado = mas_largo(&cadena1, &cadena2);
println!("La más larga es: {resultado}");
}
// Struct con referencia — el texto debe vivir más que el struct
let novela = String::from("Llámame Ishmael. Hace algunos años...");
let primera_frase;
{
let i = novela.find('.').unwrap_or(novela.len());
primera_frase = Extracto { parte: &novela[..i] };
primera_frase.mostrar();
}
}
Inicia sesión para guardar tu progreso