Validando Referencias con Lifetimes

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Validating References With Lifetimes. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, discutiremos los lifetimes y cómo aseguran que las referencias sean válidas durante el tiempo necesario. Aunque los lifetimes pueden parecer desconocidos, cubriremos las formas comunes en las que puede encontrarse la sintaxis de lifetimes para ayudarte a familiarizarte con el concepto.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100414{{"Validando Referencias con Lifetimes"}} rust/integer_types -.-> lab-100414{{"Validando Referencias con Lifetimes"}} rust/string_type -.-> lab-100414{{"Validando Referencias con Lifetimes"}} rust/function_syntax -.-> lab-100414{{"Validando Referencias con Lifetimes"}} rust/expressions_statements -.-> lab-100414{{"Validando Referencias con Lifetimes"}} rust/method_syntax -.-> lab-100414{{"Validando Referencias con Lifetimes"}} end

Validando referencias con lifetimes

Los lifetimes son otro tipo de genéricos que ya hemos estado utilizando. En lugar de garantizar que un tipo tenga el comportamiento que queremos, los lifetimes aseguran que las referencias sean válidas durante el tiempo que las necesitamos.

Un detalle que no discutimos en "Referencias y préstamos" es que cada referencia en Rust tiene un lifetime, que es el alcance durante el cual esa referencia es válida. En la mayoría de los casos, los lifetimes son implícitos e inferidos, al igual que en la mayoría de los casos, los tipos son inferidos. Solo debemos anotar los tipos cuando son posibles múltiples tipos. De manera similar, debemos anotar los lifetimes cuando los lifetimes de las referencias pueden estar relacionados de varias maneras diferentes. Rust nos obliga a anotar las relaciones utilizando parámetros de lifetime genéricos para garantizar que las referencias reales utilizadas en tiempo de ejecución definitivamente serán válidas.

La anotación de lifetimes ni siquiera es un concepto que la mayoría de los otros lenguajes de programación tienen, por lo que esto puede resultar desconocido. Aunque no cubriremos los lifetimes en su totalidad en este capítulo, discutiremos las formas comunes en las que puede encontrarse la sintaxis de lifetimes para que te sientas cómodo con el concepto.

Evitando referencias colgantes con lifetimes

El principal objetivo de los lifetimes es evitar las referencias colgantes, que hacen que un programa refiera a datos diferentes de los datos a los que está destinado a referirse. Considere el programa de la Lista 10-16, que tiene un ámbito externo y un ámbito interno.

fn main() {
  1 let r;

    {
      2 let x = 5;
      3 r = &x;
  4 }

  5 println!("r: {r}");
}

Lista 10-16: Un intento de usar una referencia cuyo valor ya ha salido de ámbito

Nota: Los ejemplos de la Lista 10-16, 10-17 y 10-23 declaran variables sin darles un valor inicial, por lo que el nombre de la variable existe en el ámbito externo. A primera vista, esto puede parecer en conflicto con el hecho de que Rust no tiene valores nulos. Sin embargo, si intentamos usar una variable antes de darle un valor, obtendremos un error en tiempo de compilación, lo que demuestra que Rust realmente no permite valores nulos.

El ámbito externo declara una variable llamada r sin valor inicial [1], y el ámbito interno declara una variable llamada x con el valor inicial de 5 [2]. Dentro del ámbito interno, intentamos establecer el valor de r como una referencia a x [3]. Luego termina el ámbito interno [4], y intentamos imprimir el valor en r [5]. Este código no se compilará porque el valor al que r se refiere ya ha salido de ámbito antes de que intentemos usarlo. Aquí está el mensaje de error:

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

El mensaje de error dice que la variable x "no tiene vida suficiente". La razón es que x estará fuera de ámbito cuando termine el ámbito interno en la línea 7. Pero r sigue siendo válido para el ámbito externo; debido a que su ámbito es más grande, decimos que "tiene una vida más larga". Si Rust permitiera que este código funcione, r estaría referenciando memoria que se desasignó cuando x salió de ámbito, y cualquier cosa que intentemos hacer con r no funcionaría correctamente. Entonces, ¿cómo determina Rust que este código es inválido? Utiliza un verificador de préstamos.

El verificador de préstamos

El compilador de Rust tiene un verificador de préstamos que compara los ámbitos para determinar si todos los préstamos son válidos. La Lista 10-17 muestra el mismo código que la Lista 10-16 pero con anotaciones que muestran los lifetimes de las variables.

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

Lista 10-17: Anotaciones de los lifetimes de r y x, denominados 'a y 'b, respectivamente

Aquí, hemos anotado el lifetime de r con 'a y el lifetime de x con 'b. Como se puede ver, el bloque interno 'b es mucho más pequeño que el bloque de lifetime externo 'a. En tiempo de compilación, Rust compara el tamaño de los dos lifetimes y ve que r tiene un lifetime de 'a pero que se refiere a memoria con un lifetime de 'b. El programa es rechazado porque 'b es más corto que 'a: el objeto de la referencia no tiene la misma duración que la referencia.

La Lista 10-18 corrige el código para que no tenga una referencia colgante y se compile sin errores.

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

Lista 10-18: Una referencia válida porque los datos tienen un lifetime más largo que la referencia

Aquí, x tiene el lifetime 'b, que en este caso es mayor que 'a. Esto significa que r puede referirse a x porque Rust sabe que la referencia en r siempre será válida mientras x sea válida.

Ahora que sabes dónde están los lifetimes de las referencias y cómo Rust analiza los lifetimes para garantizar que las referencias siempre serán válidas, exploremos los lifetimes genéricos de los parámetros y los valores de retorno en el contexto de funciones.

Lifetimes genéricos en funciones

Escribiremos una función que devuelva la cadena más larga de dos trozos de cadena. Esta función tomará dos trozos de cadena y devolverá un solo trozo de cadena. Después de implementar la función longest, el código de la Lista 10-19 debería imprimir The longest string is abcd.

Nombre de archivo: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {result}");
}

Lista 10-19: Una función main que llama a la función longest para encontrar la cadena más larga de dos trozos de cadena

Tenga en cuenta que queremos que la función tome trozos de cadena, que son referencias, en lugar de cadenas, porque no queremos que la función longest tome posesión de sus parámetros. Consulte "Trozos de cadena como parámetros" para más discusión sobre por qué los parámetros que usamos en la Lista 10-19 son los que queremos.

Si intentamos implementar la función longest como se muestra en la Lista 10-20, no se compilará.

Nombre de archivo: src/main.rs

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

Lista 10-20: Una implementación de la función longest que devuelve la cadena más larga de dos trozos de cadena pero aún no se compila

En cambio, obtenemos el siguiente error que habla sobre lifetimes:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ 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<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

El texto de ayuda revela que el tipo de retorno necesita un parámetro de lifetime genérico porque Rust no puede decir si la referencia que se devuelve se refiere a x o y. En realidad, nosotros tampoco sabemos, porque el bloque if en el cuerpo de esta función devuelve una referencia a x y el bloque else devuelve una referencia a y ¡

Cuando definimos esta función, no conocemos los valores concretos que se pasarán a esta función, por lo que no sabemos si el caso if o el caso else se ejecutará. Tampoco conocemos los lifetimes concretos de las referencias que se pasarán, por lo que no podemos mirar los ámbitos como lo hicimos en las Listas 10-17 y 10-18 para determinar si la referencia que devolvemos siempre será válida. El verificador de préstamos tampoco puede determinar esto, porque no sabe cómo los lifetimes de x e y se relacionan con el lifetime del valor de retorno. Para corregir este error, agregaremos parámetros de lifetime genéricos que definen la relación entre las referencias para que el verificador de préstamos pueda realizar su análisis.

Sintaxis de anotación de lifetimes

Las anotaciones de lifetimes no cambian la duración de ninguna de las referencias. En cambio, describen las relaciones de los lifetimes de múltiples referencias entre sí sin afectar los lifetimes. Al igual que las funciones pueden aceptar cualquier tipo cuando la firma especifica un parámetro de tipo genérico, las funciones pueden aceptar referencias con cualquier lifetime al especificar un parámetro de lifetime genérico.

Las anotaciones de lifetimes tienen una sintaxis ligeramente inusual: los nombres de los parámetros de lifetime deben comenzar con una apóstrofe (') y por lo general son todos en minúsculas y muy cortos, como los tipos genéricos. La mayoría de las personas utiliza el nombre 'a para la primera anotación de lifetime. Colocamos las anotaciones de parámetros de lifetime después del & de una referencia, usando un espacio para separar la anotación del tipo de referencia.

Aquí hay algunos ejemplos: una referencia a un i32 sin parámetro de lifetime, una referencia a un i32 que tiene un parámetro de lifetime llamado 'a, y una referencia mutable a un i32 que también tiene el lifetime 'a.

&i32        // una referencia
&'a i32     // una referencia con un lifetime explícito
&'a mut i32 // una referencia mutable con un lifetime explícito

Una anotación de lifetime por sí sola no tiene mucho significado porque las anotaciones se destinan a decirle a Rust cómo se relacionan los parámetros de lifetime genéricos de múltiples referencias entre sí. Examinemos cómo se relacionan las anotaciones de lifetime entre sí en el contexto de la función longest.

Anotaciones de lifetimes en firmas de funciones

Para usar anotaciones de lifetimes en las firmas de funciones, necesitamos declarar los parámetros de lifetime genéricos dentro de corchetes angulares entre el nombre de la función y la lista de parámetros, al igual que lo hicimos con los parámetros de tipo genéricos.

Queremos que la firma exprese la siguiente restricción: la referencia devuelta será válida siempre que ambos parámetros lo sean. Esta es la relación entre los lifetimes de los parámetros y el valor de retorno. Nombraremos el lifetime 'a y luego lo agregaremos a cada referencia, como se muestra en la Lista 10-21.

Nombre de archivo: src/main.rs

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

Lista 10-21: La definición de la función longest que especifica que todas las referencias en la firma deben tener el mismo lifetime 'a

Este código debería compilar y producir el resultado que queremos cuando lo usamos con la función main de la Lista 10-19.

La firma de la función ahora le dice a Rust que para algún lifetime 'a, la función toma dos parámetros, ambos de los cuales son trozos de cadena que viven al menos durante el lifetime 'a. La firma de la función también le dice a Rust que el trozo de cadena devuelto por la función vivirá al menos durante el lifetime 'a. En la práctica, significa que el lifetime de la referencia devuelta por la función longest es el mismo que el más corto de los lifetimes de los valores a los que se refieren los argumentos de la función. Estas relaciones son lo que queremos que Rust use al analizar este código.

Recuerde, cuando especificamos los parámetros de lifetime en esta firma de función, no estamos cambiando los lifetimes de ningún valor pasado o devuelto. En cambio, estamos especificando que el verificador de préstamos debe rechazar cualquier valor que no adhiera a estas restricciones. Tenga en cuenta que la función longest no necesita saber exactamente cuánto tiempo vivirán x e y, solo que algún ámbito puede sustituir 'a que satisfaga esta firma.

Cuando se anotan lifetimes en funciones, las anotaciones van en la firma de la función, no en el cuerpo de la función. Las anotaciones de lifetimes se convierten en parte del contrato de la función, al igual que los tipos en la firma. Tener firmas de funciones que contengan el contrato de lifetime significa que el análisis que hace el compilador de Rust puede ser más simple. Si hay un problema con la forma en que se anota una función o la forma en que se llama a ella, los errores del compilador pueden apuntar a la parte de nuestro código y las restricciones con más precisión. Si, en cambio, el compilador de Rust hiciera más inferencias sobre lo que pretendemos que sean las relaciones de los lifetimes, el compilador solo podría apuntar a un uso de nuestro código muchos pasos alejados de la causa del problema.

Cuando pasamos referencias concretas a longest, el lifetime concreto que se sustituye por 'a es la parte del ámbito de x que se superpone con el ámbito de y. En otras palabras, el lifetime genérico 'a obtendrá el lifetime concreto que es igual al más corto de los lifetimes de x e y. Debido a que hemos anotado la referencia devuelta con el mismo parámetro de lifetime 'a, la referencia devuelta también será válida durante la duración del más corto de los lifetimes de x e y.

Veamos cómo las anotaciones de lifetime restringen la función longest al pasar referencias con lifetimes concretos diferentes. La Lista 10-22 es un ejemplo sencillo.

Nombre de archivo: src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {result}");
    }
}

Lista 10-22: Usando la función longest con referencias a valores de String que tienen lifetimes concretos diferentes

En este ejemplo, string1 es válido hasta el final del ámbito externo, string2 es válido hasta el final del ámbito interno y result se refiere a algo que es válido hasta el final del ámbito interno. Ejecute este código y verá que el verificador de préstamos aprueba; se compilará y se imprimirá The longest string is long string is long.

A continuación, probemos un ejemplo que muestra que el lifetime de la referencia en result debe ser el lifetime más corto de los dos argumentos. Moveremos la declaración de la variable result fuera del ámbito interno pero dejaremos la asignación del valor a la variable result dentro del ámbito con string2. Luego moveremos el println! que usa result hacia fuera del ámbito interno, después de que haya terminado el ámbito interno. El código de la Lista 10-23 no se compilará.

Nombre de archivo: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {result}");
}

Lista 10-23: Intentando usar result después de que string2 haya salido de ámbito

Cuando intentamos compilar este código, obtenemos este error:

error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^^^^^^^^^^ borrowed value
does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                      ------ borrow later used here

El error muestra que para que result sea válido para la declaración println!, string2 debería ser válido hasta el final del ámbito externo. Rust lo sabe porque anotamos los lifetimes de los parámetros y valores de retorno de la función usando el mismo parámetro de lifetime 'a.

Como humanos, podemos ver este código y ver que string1 es más larga que string2, y por lo tanto, result contendrá una referencia a string1. Debido a que string1 aún no ha salido de ámbito, una referencia a string1 todavía será válida para la declaración println!. Sin embargo, el compilador no puede ver que la referencia es válida en este caso. Hemos dicho a Rust que el lifetime de la referencia devuelta por la función longest es el mismo que el más corto de los lifetimes de las referencias pasadas. Por lo tanto, el verificador de préstamos no permite el código de la Lista 10-23 como posiblemente tener una referencia no válida.

Intente diseñar más experimentos que varíen los valores y lifetimes de las referencias pasadas a la función longest y cómo se usa la referencia devuelta. Haga hipótesis sobre si sus experimentos pasarán el verificador de préstamos antes de compilar; luego verifique si está en lo correcto.

Pensando en términos de lifetimes

La forma en que se deben especificar los parámetros de lifetime depende de lo que hace su función. Por ejemplo, si cambiamos la implementación de la función longest para siempre devolver el primer parámetro en lugar del trozo de cadena más largo, no necesitaríamos especificar un lifetime en el parámetro y. El siguiente código se compilará:

Nombre de archivo: src/main.rs

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

Hemos especificado un parámetro de lifetime 'a para el parámetro x y el tipo de retorno, pero no para el parámetro y, porque el lifetime de y no tiene ninguna relación con el lifetime de x o el valor de retorno.

Cuando se devuelve una referencia desde una función, el parámetro de lifetime para el tipo de retorno debe coincidir con el parámetro de lifetime de uno de los parámetros. Si la referencia devuelta no se refiere a uno de los parámetros, debe referirse a un valor creado dentro de esta función. Sin embargo, esto sería una referencia colgante porque el valor saldrá de ámbito al final de la función. Considere esta implementación intentada de la función longest que no se compilará:

Nombre de archivo: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

Aquí, aunque hemos especificado un parámetro de lifetime 'a para el tipo de retorno, esta implementación no se compilará porque el lifetime del valor de retorno no está relacionado con el lifetime de los parámetros en absoluto. Aquí está el mensaje de error que obtenemos:

error[E0515]: cannot return reference to local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function

El problema es que result sale de ámbito y se limpia al final de la función longest. También estamos intentando devolver una referencia a result desde la función. No hay forma de que podamos especificar parámetros de lifetime que cambiarían la referencia colgante, y Rust no nos permitirá crear una referencia colgante. En este caso, la mejor solución sería devolver un tipo de datos con propiedad en lugar de una referencia para que la función llamante sea responsable de limpiar el valor.

En última instancia, la sintaxis de lifetime es sobre conectar los lifetimes de varios parámetros y valores de retorno de funciones. Una vez que están conectados, Rust tiene suficiente información para permitir operaciones seguras de memoria y no permitir operaciones que crearían punteros colgantes o que de otra manera violarían la seguridad de memoria.

Anotaciones de lifetimes en definiciones de struct

Hasta ahora, los structs que hemos definido todos contienen tipos con propiedad. Podemos definir structs para contener referencias, pero en ese caso necesitaríamos agregar una anotación de lifetime a cada referencia en la definición del struct. La Lista 10-24 tiene un struct llamado ImportantExcerpt que contiene un trozo de cadena.

Nombre de archivo: src/main.rs

1 struct ImportantExcerpt<'a> {
  2 part: &'a str,
}

fn main() {
  3 let novel = String::from(
        "Call me Ishmael. Some years ago..."
    );
  4 let first_sentence = novel
       .split('.')
       .next()
       .expect("Could not find a '.'");
  5 let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Lista 10-24: Un struct que contiene una referencia, requiriendo una anotación de lifetime

Este struct tiene el único campo part que contiene un trozo de cadena, que es una referencia [2]. Al igual que con los tipos de datos genéricos, declaramos el nombre del parámetro de lifetime genérico dentro de corchetes angulares después del nombre del struct para que podamos usar el parámetro de lifetime en el cuerpo de la definición del struct [1]. Esta anotación significa que una instancia de ImportantExcerpt no puede sobrevivir a la referencia que contiene en su campo part.

La función main aquí crea una instancia del struct ImportantExcerpt [5] que contiene una referencia a la primera oración de la String [4] propiedad de la variable novel [3]. Los datos en novel existen antes de que se cree la instancia de ImportantExcerpt. Además, novel no sale de ámbito hasta después de que la instancia de ImportantExcerpt sale de ámbito, por lo que la referencia en la instancia de ImportantExcerpt es válida.

Elisión de lifetimes

Has aprendido que cada referencia tiene un lifetime y que necesitas especificar parámetros de lifetime para funciones o structs que usan referencias. Sin embargo, tuvimos una función en la Lista 4-9, que se muestra nuevamente en la Lista 10-25, que se compiló sin anotaciones de lifetime.

Nombre de archivo: src/lib.rs

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Lista 10-25: Una función que definimos en la Lista 4-9 que se compiló sin anotaciones de lifetime, aunque el parámetro y el tipo de retorno son referencias

La razón por la que esta función se compila sin anotaciones de lifetime es histórica: en versiones tempranas (antes de la 1.0) de Rust, este código no se habría compilado porque cada referencia necesitaba un lifetime explícito. En aquel momento, la firma de la función se habría escrito así:

fn first_word<'a>(s: &'a str) -> &'a str {

Después de escribir mucho código de Rust, el equipo de Rust descubrió que los programadores de Rust estaban ingresando las mismas anotaciones de lifetime una y otra vez en situaciones particulares. Estas situaciones eran predecibles y seguían unos patrones deterministas. Los desarrolladores programaron estos patrones en el código del compilador para que el verificador de préstamos pudiera inferir los lifetimes en estas situaciones y no necesitara anotaciones explícitas.

Esta parte de la historia de Rust es relevante porque es posible que emerjan más patrones deterministas y se agreguen al compilador. En el futuro, se requerirán aún menos anotaciones de lifetime.

Los patrones programados en el análisis de referencias de Rust se llaman reglas de elisión de lifetimes. Estas no son reglas para que los programadores las sigan; son un conjunto de casos particulares que el compilador considerará, y si su código se ajusta a estos casos, no necesitas escribir los lifetimes explícitamente.

Las reglas de elisión no proporcionan una inferencia completa. Si Rust aplica determinísticamente las reglas pero todavía hay ambigüedad sobre qué lifetimes tienen las referencias, el compilador no adivinará cuál debería ser el lifetime de las referencias restantes. En lugar de adivinar, el compilador te dará un error que puedes resolver agregando las anotaciones de lifetime.

Los lifetimes en los parámetros de función o método se llaman lifetimes de entrada, y los lifetimes en los valores de retorno se llaman lifetimes de salida.

El compilador utiliza tres reglas para determinar los lifetimes de las referencias cuando no hay anotaciones explícitas. La primera regla se aplica a los lifetimes de entrada, y las segundas y terceras reglas se aplican a los lifetimes de salida. Si el compilador llega al final de las tres reglas y todavía hay referencias para las que no puede determinar los lifetimes, el compilador se detendrá con un error. Estas reglas se aplican a las definiciones de fn así como a los bloques impl.

La primera regla es que el compilador asigna un parámetro de lifetime a cada parámetro que es una referencia. En otras palabras, una función con un parámetro obtiene un parámetro de lifetime: fn foo<'a>(x: &'a i32); una función con dos parámetros obtiene dos parámetros de lifetime separados: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); y así sucesivamente.

La segunda regla es que, si hay exactamente un parámetro de lifetime de entrada, ese lifetime se asigna a todos los parámetros de lifetime de salida: fn foo<'a>(x: &'a i32) -> &'a i32.

La tercera regla es que, si hay múltiples parámetros de lifetime de entrada, pero uno de ellos es &self o &mut self porque se trata de un método, el lifetime de self se asigna a todos los parámetros de lifetime de salida. Esta tercera regla hace que los métodos sean mucho más fáciles de leer y escribir porque se necesitan menos símbolos.

Vamos a pretender que somos el compilador. Aplicaremos estas reglas para determinar los lifetimes de las referencias en la firma de la función first_word de la Lista 10-25. La firma comienza sin ningún lifetime asociado a las referencias:

fn first_word(s: &str) -> &str {

Luego el compilador aplica la primera regla, que especifica que cada parámetro obtiene su propio lifetime. Lo llamaremos 'a como de costumbre, así que ahora la firma es esta:

fn first_word<'a>(s: &'a str) -> &str {

La segunda regla se aplica porque hay exactamente un lifetime de entrada. La segunda regla especifica que el lifetime del único parámetro de entrada se asigna al lifetime de salida, así que ahora la firma es esta:

fn first_word<'a>(s: &'a str) -> &'a str {

Ahora todas las referencias en esta firma de función tienen lifetimes, y el compilador puede continuar su análisis sin necesidad de que el programador anote los lifetimes en esta firma de función.

Veamos otro ejemplo, esta vez usando la función longest que no tenía parámetros de lifetime cuando comenzamos a trabajar con ella en la Lista 10-20:

fn longest(x: &str, y: &str) -> &str {

Aplicemos la primera regla: cada parámetro obtiene su propio lifetime. Esta vez tenemos dos parámetros en lugar de uno, así que tenemos dos lifetimes:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

Puedes ver que la segunda regla no se aplica porque hay más de un lifetime de entrada. La tercera regla tampoco se aplica, porque longest es una función en lugar de un método, por lo que ninguno de los parámetros es self. Después de trabajar a través de las tres reglas, todavía no hemos determinado cuál es el lifetime del tipo de retorno. Esta es la razón por la que obtuvimos un error al intentar compilar el código de la Lista 10-20: el compilador trabajó a través de las reglas de elisión de lifetimes pero todavía no pudo determinar todos los lifetimes de las referencias en la firma.

Debido a que la tercera regla realmente solo se aplica en firmas de métodos, veremos los lifetimes en ese contexto a continuación para ver por qué la tercera regla significa que no tenemos que anotar los lifetimes en las firmas de métodos muy a menudo.

Anotaciones de lifetimes en definiciones de métodos

Cuando implementamos métodos en un struct con lifetimes, usamos la misma sintaxis que la de los parámetros de tipo genéricos mostrada en la Lista 10-11. Donde declaramos y usamos los parámetros de lifetime depende de si están relacionados con los campos del struct o con los parámetros y valores de retorno del método.

Los nombres de lifetime para los campos del struct siempre deben declararse después de la palabra clave impl y luego usarse después del nombre del struct porque esos lifetimes son parte del tipo del struct.

En las firmas de métodos dentro del bloque impl, las referencias pueden estar ligadas al lifetime de las referencias en los campos del struct, o pueden ser independientes. Además, las reglas de elisión de lifetimes a menudo hacen que no sea necesario anotar los lifetimes en las firmas de métodos. Veamos algunos ejemplos usando el struct llamado ImportantExcerpt que definimos en la Lista 10-24.

Primero usaremos un método llamado level cuyo único parámetro es una referencia a self y cuyo valor de retorno es un i32, que no es una referencia a nada:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

La declaración del parámetro de lifetime después de impl y su uso después del nombre del tipo es obligatoria, pero no estamos obligados a anotar el lifetime de la referencia a self debido a la primera regla de elisión.

Aquí hay un ejemplo donde se aplica la tercera regla de elisión de lifetimes:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {announcement}");
        self.part
    }
}

Hay dos lifetimes de entrada, por lo que Rust aplica la primera regla de elisión de lifetimes y le da a &self y announcement sus propios lifetimes. Luego, porque uno de los parámetros es &self, el tipo de retorno obtiene el lifetime de &self, y todos los lifetimes han sido contabilizados.

El lifetime estático

Un lifetime especial que debemos discutir es 'static, que denota que la referencia afectada puede vivir durante toda la duración del programa. Todos los literales de cadena tienen el lifetime 'static, que podemos anotar como sigue:

let s: &'static str = "I have a static lifetime.";

El texto de esta cadena se almacena directamente en el binario del programa, que siempre está disponible. Por lo tanto, el lifetime de todos los literales de cadena es 'static.

Es posible que veas sugerencias para usar el lifetime 'static en mensajes de error. Pero antes de especificar 'static como el lifetime de una referencia, piensa en si la referencia que tienes realmente vive durante toda la vida útil de tu programa o no, y si quieres que lo haga. En la mayoría de los casos, un mensaje de error que sugiere el lifetime 'static se produce al intentar crear una referencia colgante o una incompatibilidad de los lifetimes disponibles. En tales casos, la solución es corregir esos problemas, no especificar el lifetime 'static.

Parámetros de tipo genéricos, límites de trato y lifetimes juntos

Echemos un vistazo breve a la sintaxis de especificar parámetros de tipo genéricos, límites de trato y lifetimes todos en una sola función.

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {ann}");
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Esta es la función longest de la Lista 10-21 que devuelve la cadena más larga de dos trozos de cadena. Pero ahora tiene un parámetro extra llamado ann del tipo genérico T, que puede ser rellenado por cualquier tipo que implemente el trato Display como se especifica en la cláusula where. Este parámetro extra se imprimirá usando {}, por lo que el límite de trato Display es necesario. Debido a que los lifetimes son un tipo de genérico, las declaraciones del parámetro de lifetime 'a y del parámetro de tipo genérico T van en la misma lista dentro de los corchetes angulares después del nombre de la función.

Resumen

¡Felicitaciones! Has completado el laboratorio Validating References With Lifetimes. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.