El compilador de Rust necesita saber cuánto tiempo vive cada referencia para garantizar que nunca apuntas a memoria liberada. La mayoría de las veces puede inferirlo solo con las lifetime elision rules. Cuando no puede, necesitas anotar los lifetimes explícitamente con la sintaxis 'a.
El problema: referencias con duraciones distintas
fn mayor_string(s1: &str, s2: &str) -> &str { // ERROR
if s1.len() > s2.len() { s1 } else { s2 }
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:43
|
1 | fn mayor_string(s1: &str, s2: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
El compilador no sabe si la referencia retornada viene de s1 o de s2, y por tanto no sabe cuánto tiempo debe vivir. Necesitas anotarlo.
Sintaxis de lifetime annotations
fn mayor_string<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() { s1 } else { s2 }
}
fn main() {
let s1 = String::from("larga");
let resultado;
{
let s2 = String::from("xyz");
resultado = mayor_string(&s1, &s2);
println!("{}", resultado); // OK: s2 vive hasta aquí
}
// println!("{}", resultado); // ERROR: s2 ya no existe
}
La anotación 'a dice: "la referencia retornada vivirá como máximo lo que viva la más corta de las dos entradas". El compilador usa esto para verificar que no hay dangling references.
Lifetimes en structs
struct Extracto<'a> {
contenido: &'a str,
}
impl<'a> Extracto<'a> {
fn mostrar(&self) {
println!("{}", self.contenido);
}
}
fn main() {
let novela = String::from("Era una noche oscura...");
let primera_frase = novela.split('.').next().expect("al menos una frase");
let e = Extracto { contenido: primera_frase };
e.mostrar();
// e no puede vivir más que novela
}
Lifetime elision rules
El compilador aplica estas tres reglas y si puede determinar todos los lifetimes de salida, no necesitas anotarlos:
- Cada referencia de entrada obtiene su propio lifetime:
fn f(x: &T, y: &U)?fn f<'a, 'b>(x: &'a T, y: &'b U). - Si hay un único parámetro de entrada, su lifetime se aplica a todas las salidas.
- Si hay un parámetro
&selfo&mut self, su lifetime se aplica a todas las salidas.
// No necesita anotación: se aplica la regla 2
fn primera_palabra(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// No necesita anotación: se aplica la regla 3
impl Extracto<'_> {
fn contenido(&self) -> &str {
self.contenido
}
}
El lifetime estático: 'static
// Los literales de string tienen lifetime 'static: viven todo el programa
let s: &'static str = "Hola, mundo";
// También lo tienen las constantes globales
static SALUDO: &str = "Hola";
Lifetimes, generics y trait bounds juntos
use std::fmt::Display;
fn mayor_con_anuncio<'a, T: Display>(s1: &'a str, s2: &'a str, anuncio: T) -> &'a str {
println!("Anuncio: {}", anuncio);
if s1.len() > s2.len() { s1 } else { s2 }
}
Resumen
- Los lifetimes le dicen al compilador cuánto deben vivir las referencias.
- Sintaxis:
'aen la firma de función, struct o impl. 'aen retorno significa "vive al menos tanto como la entrada con el mismo lifetime".- Las elision rules infieren lifetimes en los casos más comunes: no necesitas anotarlos.
'static: el lifetime del programa completo.
El siguiente artículo cubre los traits esenciales de la stdlib: Display, Debug, From, Into, PartialEq, Ord y Default, que encontrarás en casi cualquier código Rust.
