3 votes

Questions on understanding lifetimes

J'ai eu du mal à comprendre les durées de vie et j'apprécierais un peu d'aide pour comprendre certaines subtilités qui sont généralement absentes des ressources et des autres questions/réponses ici. Même la section entière du Livre est trompeuse car son exemple principal utilisé comme justification derrière les durées de vie est plus ou moins faux (c'est-à-dire que le compilateur peut très facilement déduire les durées de vie sur la fonction mentionnée).


En prenant cette fonction (un peu similaire au livre) comme exemple:

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

Ma compréhension est que les durées de vie explicites affirment que la référence renvoyée ne doit pas vivre plus longtemps que la plus courte des durées de vie de x et y. Autrement dit, à la fois x et y devraient survivre à la référence renvoyée. (Bien que je ne sois pas du tout sûr de ce que le compilateur fait exactement, vérifie-t-il les durées de vie des arguments puis compare le minimum avec la durée de vie de la référence renvoyée ?)

Mais alors, que signifierait la durée de vie si nous n'avions pas de valeurs de retour ? Cela implique-t-il un sens spécial (par exemple, par rapport à l'utilisation de deux durées de vie différentes ?)

fn foo<'a>(x: &'a str, y: &'a str) {

}

Et ensuite nous avons des structures telles que:

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

Et

struct Foo<'a> {
    x: &'a i32,
    y: &'a i32,
}

Il semble que l'utilisation de la même durée de vie pour les champs ajoute des contraintes, mais quelle est exactement cette contrainte qui cause certains exemples de ne pas fonctionner?


Cela pourrait nécessiter une question à part entière, mais il y a beaucoup de mentions de durées de vie et de portées étant différentes mais sans beaucoup d'explications, y a-t-il des ressources approfondissant cela, en particulier en considérant les durées de vie non lexicales ?

6voto

rodrigo Points 34500

Pour comprendre les durées de vie, vous devez noter qu'elles font en réalité partie du type, et non de la valeur. C'est pourquoi elles sont spécifiées en tant que paramètres génériques.

Autrement dit, lorsque vous écrivez :

fn test(a: &i32) {
    let i: i32 = 0;
    let b: &i32 = &i;
    let c: &'static i32 = &0;
}

alors les variables a, b et c sont en réalité de types différents : un type est &'__unnamed_1 i32, l'autre est &_unnamed_2 i32 et l'autre est &'static i32.

La chose amusante est que les durées de vie créent une hiérarchie de types, de sorte que lorsqu'un type vit plus longtemps qu'un autre type, mais à part cela qu'ils sont identiques, alors celui qui vit longtemps est un sous-type de celui qui vit moins longtemps.

En particulier, dans un cas d'héritage multiple extrême, le type &'static i32 est un sous-type de tout autre &'_ i32.

Vous pouvez vérifier que les sous-types Rust sont réels avec cet exemple :

fn test(mut a: &i32) {
    let i: i32 = 0;
    let mut b: &i32 = &i;
    let mut c: &'static i32 = &0;
    //c est un sous-type de a et b
    //a est un sous-type de b
    a = c; // ok
    c = a; // erreur
    b = a; // ok
    a = b; // erreur
}

Il convient de noter que les durées de vie sont un problème de vérificateur d'emprunt. Une fois qu'il est satisfait et que le code est prouvé sûr, les durées de vie sont effacées et la génération de code est effectuée à l'aveuglette, en supposant que tous les accès aux valeurs de la mémoire sont valides. C'est pourquoi, même si les durées de vie sont des paramètres génériques, foo<'a>() n'est instancié qu'une seule fois.

Revenons à vos exemples :

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

Vous pouvez appeler cette fonction avec des valeurs de différentes durées de vie, car le compilateur déduira 'a comme étant la plus courte des deux, de sorte que &'a str sera un super-type de l'autre :

    let s = String::from("hello");
    let r = foo(&s, "world");

Ceci est équivalent à (syntaxe inventée pour l'annotation de durée de vie) :

    let s: &'s str = String::from("hello");
    let r: &'s str = foo::<'s>(&s, "world" as &'s str);

À propos des structures avec des durées de vie multiples, cela n'a généralement pas d'importance, et je déclare généralement toutes les durées de vie de la même manière, en particulier si le type est privé à ma caisse.

Mais pour les types génériques publics, il peut être utile de déclarer plusieurs durées de vie, en particulier parce que l'utilisateur peut vouloir rendre certaines d'entre elles 'static.

Par exemple :

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b str,
}

struct Bar<'a> {
    x: &'a i32,
    y: &'a str,
}

L'utilisateur de Foo peut l'utiliser comme :

let x = 42;
let foo = Foo { x: &x, y: "hello" };

Mais l'utilisateur de Bar doit allouer une String, ou faire de la magie de str allouée sur la pile :

let x = 42;
let y = String::from("hello");
let bar = Bar { x: &x, y: &y };

Remarquez que Foo peut être utilisé exactement comme Bar mais pas vice versa.

De plus, l'impl Foo peut fournir des fonctions supplémentaires qui ne sont pas possibles pour l'impl Bar :

impl<'a> Foo<'a, 'static> {
    fn get_static_str(&self) -> &'static str {
        self.y
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X