Relaciones de vida entre parámetros y valores de retorno
En Rust, los ciclos de vida aseguran que las referencias no sobrevivan a los datos a los que apunten. Cuando una función recibe referencias como argumentos y devuelve otra referencia, el sistema de tipos necesita saber cómo se relacionan estos ciclos de vida.
Por ejemplo, considera esta función:
fn obtener_primero<'a>(x: &'a str, y: &str) -> &'a str {
x
}
Aquí solo x lleva una anotación de ciclo de vida porque el valor devuelto está vinculado al primer parámetro. No es necesario restringir y, ya que no influye en el resultado. Esto muestra que las firmas deben reflejar la lógica interna: si el retorno depende exclusivamente de un parámetro, solo ese parámetro requiere anotación.
Retornar datos desde dentro de una función
Un error común ocurre cuando intentamos devolver una referencia a un valor creado dentro del ámbito de la función:
fn crear_texto() -> &str {
let temp = String::from("dato local");
temp.as_str()
}
Este código falla porque temp se destruye al finalizar la función, dejando una referencia colgante. La solución es transferir propiedad del valor completo:
fn crear_texto() -> String {
String::from("dato transferido")
}
Al retornar String en lugar de &str, cedemos el control del dato al llamador sin involucrar ciclos de vida explícitos, ya que ya no manejamos referencias.
Campos con referencias en estructuras
También podemos definir structs que contengan referencias, pero en esos casos es obligatorio especificar ciclos de vida para garantizar que la referencia vivirá más que la instancia del struct. Por ejemplo:
struct ExtractoImportante<'a> {
contenido: &'a str,
}
Esta declaración indica que cualquier instancia de ExtractoImportante no puede vivir más tiempo que la referencia almacenada en contenido. Un uso correcto sería:
fn main() {
let texto = String::from("Había una vez...");
let oracion = texto.split('.').next().unwrap();
let extracto = ExtractoImportante { contenido: oracion };
}
Como oracion proviene de texto y ambos viven lo suficiante, la instancia extracto es segura.
Inferencia automática de ciclos de vida
No todas las funciones necesitan anotaciones de ciclo de vida. Rust incluye reglas integradas que permiten al compilador deducirlas en ciertos patrones comunes. Este mecanismo se llama omisión de ciclos de vida.
Por ejemplo, esta función compila sin anotaciones:
fn primera_palabra(s: &str) -> &str {
for (i, &b) in s.as_bytes().iter().enumerate() {
if b == b' ' {
return &s[..i];
}
}
&s[..]
}
Aunque parece que faltan ciclos de vida, el compilador aplica tres reglas predefinidas para inferirlos automáticamente.
Las tres reglas de omisión
- Regla 1: Cada parámetro que sea una referencia obtiene su propio parámetro de ciclo de vida. Así, una función con dos referencias tendrá
'ay'b. - Regla 2: Si hay exactamente un parámetro de entrada por referencia, ese ciclo de vida se asigna a todos los valores de retorno por referencia.
- Regla 3: Si hay múltiples parámetros de entrada, y uno de ellos es
&selfo&mut self(es decir, un método), entonces el ciclo de vida deselfse asigna a todos los retornos por referencia.
Ejemplo exitoso: aplicación de reglas
Dado:
fn primera_palabra(s: &str) -> &str
Aplicando Regla 1: un solo parámetro → 'a. Queda: fn primera_palabra<'a>(s: &'a str) -> &str.
Regla 2: hay un solo parámetro de entrada, así que el retorno también es 'a. Resultado final: fn primera_palabra<'a>(s: &'a str) -> &'a str.
Regla 3 no aplica porque no es un método.
Ejemplo fallido: ambigüedad inevitable
Considera ahora:
fn mas_largo(x: &str, y: &str) -> &str
Regla 1: dos parámetros → 'a y 'b.
Regla 2: no aplica (más de un parámetro).
Regla 3: no aplica (no es un método).
El retorno sigue sin ciclo de vida deifnido. El compilador no puede decidir si debe ser 'a o 'b, por lo tanto exige una anotación manual:
fn mas_largo<'a>(x: &'a str, y: &'a str) -> &'a str