Dangling references en Rust: por qué el compilador impide retornar referencias locales

Una dangling reference (referencia colgante) es un puntero que apunta a memoria que ya ha sido liberada. En C y C++ el compilador te deja crearlas sin advertir, y el resultado es corrupción de memoria o comportamiento indefinido. En Rust, el compilador las detecta en tiempo de compilación y rechaza el programa antes de que se ejecute una sola línea.

El problema: retornar una referencia a una variable local

fn crear_referencia() -> &String { // ERROR
    let s = String::from("hola");
    &s
} // s sale del scope y se destruye aquí

fn main() {
    let r = crear_referencia(); // r apuntaría a memoria liberada
    println!("{}", r);
}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:30
  |
1 | fn crear_referencia() -> &String {
  |                          ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value,
    but there is no value for it to be borrowed from

El compilador detecta que estás intentando retornar una referencia a algo que no sobrevivirá más allá de la función. El error E0106 indica que falta un lifetime specifier: el compilador no puede inferir de dónde viene la referencia retornada porque no hay ningún valor que pueda prestarla.

En C++ el mismo código se compila y falla en runtime

// C++ — compila con warnings, comportamiento indefinido
std::string* crear_referencia() {
    std::string s = "hola";
    return &s; // dangling pointer
}

int main() {
    std::string* p = crear_referencia();
    std::cout << *p; // undefined behavior
}

En Rust ese mismo programa es imposible de compilar. No hay runtime check: el análisis es estático.

Solución 1: retornar el valor en lugar de la referencia

La solución más sencilla es transferir la propiedad al caller en lugar de prestar una referencia:

fn crear_string() -> String { // ownership, no referencia
    let s = String::from("hola");
    s // se mueve al caller
}

fn main() {
    let s = crear_string();
    println!("{}", s); // OK
}

El String no se destruye al salir de la función porque su propiedad se transfiere al caller antes de que el scope termine.

Solución 2: lifetime annotations

Si la referencia retornada viene de un parámetro de entrada (es decir, el dato vive fuera de la función), puedes anotar el lifetime para que el compilador lo sepa:

fn primera_palabra<'a>(s: &'a str) -> &'a str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

fn main() {
    let frase = String::from("hola mundo");
    let palabra = primera_palabra(&frase);
    println!("{}", palabra); // OK: palabra vive mientras frase
}

La anotación 'a le dice al compilador: "la referencia retornada vivirá al menos tanto como la referencia de entrada". Así puede verificar que no haya dangling references.

Cuándo aparece el error E0106

E0106 aparece cuando:

  • Retornas una referencia desde una función sin indicar de dónde viene.
  • Guardas una referencia en un struct sin anotar su lifetime.
  • El compilador no puede inferir automáticamente el lifetime (las reglas de elision no se aplican).

Structs con referencias

// Sin lifetime annotation ? error
struct Extracto {
    contenido: &str, // ERROR: necesita lifetime
}

// Con lifetime annotation ? OK
struct Extracto<'a> {
    contenido: &'a str,
}

fn main() {
    let novela = String::from("Era una noche oscura y tormentosa...");
    let primera = novela.split('.').next().unwrap();
    let extracto = Extracto { contenido: primera };
    println!("{}", extracto.contenido);
}

Resumen

  • Una dangling reference apunta a memoria ya liberada: causa UB en C/C++, error de compilación en Rust.
  • No puedes retornar una referencia a una variable local: el compilador rechaza el programa con E0106.
  • Solución más simple: retornar el valor con ownership en lugar de una referencia.
  • Cuando la referencia viene de un parámetro, usa lifetime annotations para que el compilador pueda verificarlo.

Las lifetime annotations se tratan en profundidad más adelante en la serie. El siguiente artículo explora los enums de Rust: cómo modelar datos con variantes que pueden llevar información distinta, algo mucho más potente que los enums de C o Java.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP