rust - ¿Por qué el nombre de vida aparece como parte del tipo de función?
lifetime (3)
¿Qué significa la anotación <''a> después del nombre de la función?
fn substr<''a>(s: &''a str, until: u32) -> &''a str;
^^^^
Este es un parámetro de vida genérico. Es similar a un parámetro de tipo genérico (a menudo visto como <T>
), en el sentido de que la persona que llama a la función decide cuál es la vida útil. Como ha dicho, la duración del resultado será la misma que la duración del primer argumento.
Todos los nombres de por vida son equivalentes, excepto uno: ''static
. Esta vida está preestablecida para significar "garantizado para vivir durante toda la vida del programa".
El nombre de parámetro de vida más común es probablemente ''a
, pero puede usar cualquier letra o cadena. Las letras individuales son las más comunes, pero cualquier identificador de snake_case
es aceptable.
¿Por qué el compilador lo necesita y qué hace con él?
El óxido generalmente favorece que las cosas sean explícitas, a menos que haya un beneficio ergonómico muy bueno. Durante toda la vida, elision de por vida se ocupa de algo así como el 85% de los casos, lo que parecía una clara victoria.
Los parámetros de tipo viven en el mismo espacio de nombres que otros tipos: ¿es T
un tipo genérico o alguien le dio un nombre a esta estructura? Por lo tanto, los parámetros de tipo deben tener una anotación explícita que muestre que T
es un parámetro y no un tipo real. Sin embargo, los parámetros de vida útil no tienen este mismo problema, por lo que no es la razón.
En cambio, el principal beneficio de listar explícitamente los parámetros de tipo es porque puede controlar cómo interactúan los múltiples parámetros. Un ejemplo sin sentido:
fn better_str<''a, ''b, ''c>(a: &''a str, b: &''b str) -> &''c str
where ''a: ''c,
''b: ''c,
{
if a.len() < b.len() { a } else { b }
}
Tenemos dos cadenas y decimos que las cadenas de entrada pueden tener tiempos de vida diferentes, pero ambos deben superar la vida útil del valor del resultado.
Otro ejemplo, como señala DK , es que las estructuras pueden tener sus propias vidas. Hice este ejemplo también un poco de tontería, pero espero que transmita el punto:
struct Player<''a> {
name: &''a str,
}
fn name<''p, ''n>(player: &''p Player<''n>) -> &''n str {
player.name
}
Las vidas pueden ser una de las partes más desconcertantes de Rust, pero son muy buenas cuando empiezas a comprenderlas.
Creo que esta declaración de función le dice a Rust que la duración de la salida de la función es la misma que la duración de su parámetro s
:
fn substr<''a>(s: &''a str, until: u32) -> &''a str;
^^^^
Me parece que el compilador solo necesita saber esto (1):
fn substr(s: &''a str, until: u32) -> &''a str;
¿Qué significa la anotación <''a>
después del nombre de la función? ¿Por qué el compilador lo necesita y qué hace con él?
(1): Sé que necesita saber aún menos, debido a la elección de por vida. Pero esta pregunta es acerca de especificar explícitamente el tiempo de vida.
La anotación <''a>
simplemente declara los tiempos de vida utilizados en la función, exactamente como los parámetros genéricos <T>
.
fn subslice<''a, T>(s: &''a [T], until: u32) -> &''a [T] { //'
&s[..until as usize]
}
Tenga en cuenta que en su ejemplo, todas las vidas se pueden inferir.
fn subslice<T>(s: &[T], until: u32) -> &[T] {
&s[..until as usize]
}
fn substr(s: &str, until: u32) -> &str {
&s[..until as usize]
}
Permítanme ampliar las respuestas anteriores ...
¿Qué significa la anotación <''a> después del nombre de la función?
No usaría la palabra "anotación" para eso. Al igual que <T>
introduce un parámetro de tipo genérico, <''a>
introduce un parámetro de duración genérico. No puede usar ningún parámetro genérico sin introducirlos primero y, para las funciones genéricas, esta introducción ocurre justo después de su nombre. Puedes pensar en una función genérica como una familia de funciones. Entonces, esencialmente, obtienes una función para cada combinación de parámetros genéricos. substr::<''x>
sería un miembro específico de esa familia de funciones por algún tiempo ''x
vida ''x
.
Si no tiene claro cuándo y por qué tenemos que ser explícitos acerca de las vidas, siga leyendo ...
Un parámetro de vida siempre está asociado con todos los tipos de referencia. Cuando escribes
fn main() {
let x = 28374;
let r = &x;
}
el compilador sabe que x vive en el ámbito de la función principal incluido con llaves. Internamente, identifica este alcance con algún parámetro de vida útil. Para nosotros, no tiene nombre. Cuando tome la dirección de x
, obtendrá un valor de un tipo de referencia específico. Un tipo de referencia es una clase de miembro de una familia bidimensional de tipos de referencia. Un eje es el tipo al que apunta la referencia y el otro eje es una vida útil que se utiliza para dos restricciones:
- El parámetro de vida útil de un tipo de referencia representa un límite superior durante el tiempo que puede conservar esa referencia
- El parámetro de duración de un tipo de referencia representa un límite inferior para la duración de las cosas a las que puede hacer referencia el punto.
Juntas, estas restricciones juegan un papel vital en la historia de seguridad de la memoria de Rust. El objetivo aquí es evitar referencias colgantes. Nos gustaría descartar las referencias que apuntan a alguna región de memoria que ya no podemos usar porque esa cosa que solía apuntar ya no existe.
Una posible fuente de confusión es probablemente el hecho de que los parámetros de vida útil son invisibles la mayor parte del tiempo. Pero eso no significa que no estén allí. Las referencias siempre tienen un parámetro de por vida en su tipo. Pero tal parámetro de vida útil no tiene que tener un nombre y la mayoría de las veces no necesitamos mencionarlo de todos modos porque el compilador puede asignar nombres para los parámetros de vida automáticamente. Esto se llama "elision de por vida". Por ejemplo, en el siguiente caso, no ve ningún parámetro de vida útil mencionado:
fn substr(s: &str, until: u32) -> &str {…}
Pero está bien escribirlo así. En realidad es una sintaxis abreviada para los más explícitos.
fn substr<''a>(s: &''a str, until: u32) -> &''a str {…}
Aquí, el compilador asigna automáticamente el mismo nombre al "tiempo de vida de entrada" y al "tiempo de vida de salida" porque es un patrón muy común y lo más probable es que sea exactamente lo que desea. Debido a que este patrón es tan común, el compilador nos permite alejarnos sin decir nada acerca de las vidas. Suponemos que esta forma más explícita es lo que queremos decir en función de un par de reglas de "elision de por vida" (que al menos están documentadas here )
Hay situaciones en las que los parámetros de vida explícitos no son opcionales. Por ejemplo, si escribes
fn min<T: Ord>(x: &T, y: &T) -> &T {
if x <= y {
x
} else {
y
}
}
el compilador se quejará porque interpretará la declaración anterior como
fn min<''a, ''b, ''c, T: Ord>(x: &''a T, y: &''b T) -> &''c T { … }
Por lo tanto, para cada referencia se introduce un parámetro de vida por separado. Pero no hay información disponible sobre cómo se relacionan entre sí los parámetros de vida útil en esta firma. El usuario de esta función genérica podría utilizar cualquier duración. Y eso es un problema dentro de su cuerpo. Estamos intentando devolver x
o y
. Pero el tipo de x
es &''a T
Eso no es compatible con el tipo de retorno &''c T
Lo mismo es cierto para y
. Como el compilador no sabe cómo se relacionan estas vidas, no es seguro devolver estas referencias como una referencia de tipo &''c T
¿Puede ser seguro pasar de un valor de tipo &''a T
a &''c T
? Sí. Es seguro si la vida útil ''a
es igual o mayor que la vida útil ''c
. O en otras palabras, ''a: ''c
. Entonces, podríamos escribir esto
fn min<''a, ''b, ''c, T: Ord>(x: &''a T, y: &''b T) -> &''c T
where ''a: ''c, ''b: ''c
{ … }
y salirse con la suya sin que el compilador se queje del cuerpo de la función. Pero en realidad es innecesariamente complejo. También podemos simplemente escribir
fn min<''a, T: Ord>(x: &''a T, y: &''a T) -> &''a T { … }
y usar un solo parámetro de por vida para todo. El compilador puede deducir ''a
como el tiempo de vida mínimo de las referencias de argumentos en el sitio de la llamada solo porque usamos el mismo nombre de tiempo de vida para ambos parámetros. Y esta vida es precisamente lo que necesitamos para el tipo de retorno.
Espero que esto responda tu pregunta. :) ¡Salud!