Validación de referencias mediante el uso de duraciones

Completado

El uso de referencias plantea un problema. El elemento al que se refiere una referencia no realiza un seguimiento de todas sus referencias. Este comportamiento puede generar un problema: cuando se anula el elemento y sus recursos se liberan, ¿cómo se puede tener la seguridad de que no haya referencias que apunten a la memoria ya liberada (y, por tanto, no válida)?

Lenguajes como C y C++ suelen tener este problema en el que un puntero apunta a un elemento que ya se ha liberado. Esto se conoce como "puntero colgante". Afortunadamente, Rust elimina este problema. Garantiza que todas las referencias siempre se refieran a elementos válidos. Pero, ¿cómo lo hace?

La respuesta de Rust a esta pregunta son las duraciones. Permiten que Rust garantice la seguridad de la memoria sin los costos de rendimiento de la recolección de elementos no utilizados.

Considere el siguiente fragmento de código, que intenta utilizar una referencia cuyo valor ha salido del ámbito:

fn main() {
    let x;
    {
        let y = 42;
        x = &y; // We store a reference to `y` in `x` but `y` is about to be dropped.
    }
    println!("x: {}", x); // `x` refers to `y` but `y has been dropped!
}

No se puede compilar el código anterior y aparece el mensaje de error siguiente:

    error[E0597]: `y` does not live long enough
     --> src/main.rs:6:17
      |
    6 |             x = &y;
      |                 ^^ borrowed value does not live long enough
    7 |         }
      |         - `y` dropped here while still borrowed
    8 |         println!("x: {}", x);
      |                           - borrow later used here

Este error se produce porque se colocó un valor mientras estaba prestado. En este caso, y se coloca al final del ámbito interno, pero x lo toma prestado hasta la llamada println. Dado que x sigue siendo válido para el ámbito externo (porque su ámbito es mayor), suponemos que "vive más tiempo".

Este es el mismo fragmento de código con dibujos en torno a cada duración variable. Hemos asignado un nombre a cada duración:

  • 'a es la anotación de duración de nuestro valor x.
  • 'b es la anotación de duración de nuestro valor y.
fn main() {
    let x;                // ---------+-- 'a
    {                     //          |
        let y = 42;       // -+-- 'b  |
        x = &y;           //  |       |
    }                     // -+       |
    println!("x: {}", x); //          |
}

Aquí podemos ver que el bloque de duración 'b interno es más corto que el bloque 'a externo.

El compilador de Rust puede comprobar si los préstamos son válidos mediante el comprobador de préstamos. El comprobador de préstamos compara las dos duraciones en tiempo de compilación. En este escenario, x tiene una duración de 'a, pero hace referencia a un valor con una duración de 'b. El sujeto de referencia (y en la duración 'b) tiene un tiempo más corto que la referencia (x en la duración 'a), así que el programa no se compila.

Anotación de las duraciones en funciones

Al igual que con los tipos, las duraciones se deducen mediante el compilador de Rust.

Puede haber varias duraciones. Cuando esto ocurra, anótelas para ayudar al compilador a comprender cuál se usará para garantizar que las referencias sean válidas en tiempo de ejecución.

Por ejemplo, considere una función que toma dos cadenas como parámetros de entrada y devuelve las más largas:

fn main() {
    let magic1 = String::from("abracadabra!");
    let magic2 = String::from("shazam!");

    let result = longest_word(&magic1, &magic2);
    println!("The longest magic word is {}", result);
}

fn longest_word(x: &String, y: &String) -> &String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

El código anterior no se puede compilar y aparece un mensaje de error informativo:

    error[E0106]: missing lifetime specifier
     --> src/main.rs:9:38
      |
    9 | fn longest_word(x: &String, y: &String) -> &String {
      |                    ----        ----        ^ expected named lifetime parameter
      |
      = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
    help: consider introducing a named lifetime parameter
      |
    9 | fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
      |                ^^^^    ^^^^^^^        ^^^^^^^        ^^^

El texto de ayuda indica que Rust no puede decir si la referencia que se devuelve hace referencia a x o a y. Nosotros tampoco. Por lo tanto, para ayudar a identificar cuál es la referencia, anote el tipo de valor devuelto con un parámetro genérico que represente la duración.

Es posible que las duraciones sean diferentes cada vez que se llama a la función. No se conocen las duraciones concretas de las referencias que se pasarán a la función longest_word y no se puede determinar si la referencia que se va a devolver siempre será válida.

El comprobador de préstamos tampoco puede determinar si la referencia será una válida. Desconoce cómo la duración de los parámetros de entrada se relaciona con la duración del valor devuelto. Esta ambigüedad es el motivo por el que es necesario anotar las duraciones manualmente.

Afortunadamente, el compilador nos ha proporcionado una sugerencia sobre cómo corregir este error. Podemos agregar parámetros de duración genéricos a la signatura de función. Estos parámetros definen la relación entre las referencias para que el comprobador de préstamos pueda realizar su análisis:

fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Puede probar este código en el área de juegos de Rust.

Asegúrese de declarar parámetros de duración genéricos entre corchetes angulares y agregue la declaración entre la lista de parámetros y el nombre de la función.

Nota:

En la signatura, el valor devuelto y todas las referencias de parámetro deben tener la misma duración. Por lo tanto, use el mismo nombre de duración, por ejemplo 'a. Luego, agregue el nombre a cada referencia de la signatura de función.

En este caso, no hay nada especial acerca del nombre 'a. Sería igual de correcto usar cualquier otra palabra, como 'response o 'program. Lo importante que debe tenerse en cuenta es que todos los parámetros y el valor devuelto estarán activos al menos mientras esté vigente la duración asociada a cada uno de ellos.

Vamos a experimentar con este código de ejemplo y a cambiar algunos valores y la duración de las referencias que se pasan a la función longest_word para ver cómo se comporta. El compilador también rechazaría el siguiente fragmento de código, pero, ¿puede adivinar por qué?

fn main() {
    let magic1 = String::from("abracadabra!");
    let result;
    {
        let magic2 = String::from("shazam!");
        result = longest_word(&magic1, &magic2);
    }
    println!("The longest magic word is {}", result);
}

fn longest_word<'a>(x: &'a String, y: &'a String) -> &'a String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Encontrará este fragmento de código en el área de juegos de Rust.

Si ha adivinado que este código está roto, está en lo cierto. Esta vez, vemos el siguiente error:

    error[E0597]: `magic2` does not live long enough
     --> src/main.rs:6:40
      |
    6 |         result = longest_word(&magic1, &magic2);
      |                                        ^^^^^^^ borrowed value does not live long enough
    7 |     }
      |     - `magic2` dropped here while still borrowed
    8 |     println!("The longest magic word is {}", result);
      |                                              ------ borrow later used here

Este error muestra que el compilador esperaba que la duración de magic2 fuera la misma que la duración del valor devuelto y del argumento de entrada x. Rust esperaba este comportamiento porque se han anotado las duraciones de los parámetros de función y el valor devuelto con el mismo nombre de duración: 'a.

Si inspeccionáramos el código, como humanos, veríamos que magic1 es más largo que magic2. Veríamos que el resultado contiene una referencia a magic1, que será lo suficientemente larga como para ser válida. Sin embargo, Rust no puede ejecutar ese código en tiempo de compilación. Considerará que las referencias &magic1 y &magic2 son posibles valores devueltos y emitirá el error que vimos anteriormente.

La duración de la referencia que devuelve la función longest_word coincide con la más pequeña de las duraciones de las referencias que se pasan. Por lo tanto, es posible que el código incluya una referencia no válida y el corrector de préstamos no la permita.

Anotación de las duraciones en tipos

Cada vez que un struct o una enumeración contienen una referencia en uno de sus campos, debemos anotar esa definición de tipo con la duración de cada referencia que lleve a cabo con ella.

Por ejemplo, observe el siguiente código de ejemplo. Tenemos un cadena text(que posee su contenido) y un struct de tupla Highlight. El struct tiene un campo que contiene un segmento de cadena. El segmento es un valor prestado de otra parte del programa.

#[derive(Debug)]
struct Highlight<'document>(&'document str);

fn main() {
    let text = String::from("The quick brown fox jumps over the lazy dog.");
    let fox = Highlight(&text[4..19]);
    let dog = Highlight(&text[35..43]);
    println!("{:?}", fox);
    println!("{:?}", dog);
}

El código anterior está disponible en el área de juegos de Rust.

Colocamos el nombre del parámetro de duración genérico entre corchetes angulares después del nombre de la estructura. Esta ubicación es para poder usar el parámetro de duración en el cuerpo de la definición de estructura. Esta instancia de Highlight no puede durar más que la referencia de su campo debido a la declaración.

En el código anterior, hemos anotado el struct con una duración llamada 'document. Esta anotación es un recordatorio de que el struct Highlight no puede sobrevivir al origen de &str que toma prestado, un documento supuesto.

Aquí, la función main crea dos instancias del struct Highlight. Cada instancia contiene una referencia a un segmento del valor String que pertenece a la variable text:

  • fox hace referencia al segmento entre los caracteres 4º y 19º de la cadena text.
  • dog hace referencia al segmento entre los caracteres 35º y 43º de la cadena text.

Además, Highlight sale del ámbito antes que text, lo que significa que la instancia de Highlight es válida.

El código imprimiría este mensaje en la consola:

Highlight("quick brown fox")
Highlight("lazy dog")

Como experimento, intente transferir el valor que text contiene fuera del ámbito y vea qué tipos de mensajes emite el comprobador de préstamos:

#[derive(Debug)]
struct Highlight<'document>(&'document str);

fn erase(_: String) { }

fn main() {
    let text = String::from("The quick brown fox jumps over the lazy dog.");
    let fox = Highlight(&text[4..19]);
    let dog = Highlight(&text[35..43]);

    erase(text);

    println!("{:?}", fox);
    println!("{:?}", dog);
}

Puede encontrar este fragmento de código con error en el área de juegos de Rust.