web-dev-qa-db-fra.com

Pourquoi ne puis-je pas stocker une valeur et une référence à cette valeur dans la même structure?

J'ai une valeur et je veux stocker cette valeur et une référence à quelque chose à l'intérieur de cette valeur dans mon propre type:

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

Parfois, j'ai une valeur et je veux stocker cette valeur et une référence à cette valeur dans la même structure:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

Parfois, je ne prends même pas une référence de la valeur et j'obtiens la même erreur:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

Dans chacun de ces cas, j'obtiens une erreur indiquant que l'une des valeurs "ne vit pas assez longtemps". Que signifie cette erreur?

185
Shepmaster

Regardons ne implémentation simple de ceci :

struct Parent {
    count: u32,
}

struct Child<'a> {
    parent: &'a Parent,
}

struct Combined<'a> {
    parent: Parent,
    child: Child<'a>,
}

impl<'a> Combined<'a> {
    fn new() -> Self {
        let parent = Parent { count: 42 };
        let child = Child { parent: &parent };

        Combined { parent, child }
    }
}

fn main() {}

Cela échouera avec l'erreur:

error[E0515]: cannot return value referencing local variable `parent`
  --> src/main.rs:19:9
   |
17 |         let child = Child { parent: &parent };
   |                                     ------- `parent` is borrowed here
18 | 
19 |         Combined { parent, child }
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `parent` because it is borrowed
  --> src/main.rs:19:20
   |
14 | impl<'a> Combined<'a> {
   |      -- lifetime `'a` defined here
...
17 |         let child = Child { parent: &parent };
   |                                     ------- borrow of `parent` occurs here
18 | 
19 |         Combined { parent, child }
   |         -----------^^^^^^---------
   |         |          |
   |         |          move out of `parent` occurs here
   |         returning this value requires that `parent` is borrowed for `'a`

Pour bien comprendre cette erreur, vous devez réfléchir à la façon dont les valeurs sont représentées en mémoire et à ce qui se passe lorsque vous move ces valeurs. Annotons Combined::new Avec des adresses mémoire hypothétiques indiquant où se trouvent les valeurs:

let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000

Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?

Que devrait-il arriver à child? Si la valeur venait juste d'être déplacée comme parent, elle ferait alors référence à la mémoire dont la garantie n'est plus d'avoir une valeur valide. Tout autre élément de code est autorisé à stocker des valeurs à l'adresse de mémoire 0x1000. L'accès à cette mémoire en supposant qu'il s'agit d'un entier peut entraîner des pannes et/ou des problèmes de sécurité. Il s'agit de l'une des principales catégories d'erreurs empêchées par Rust.

C’est exactement le problème que durée de vie empêche. Une durée de vie est un peu de métadonnée qui permet au compilateur de savoir combien de temps une valeur sera valide à son emplacement de mémoire actuel . C'est une distinction importante, car c'est une erreur commune que Rust commettent. Les durées de vie Rust sont not = la période entre le moment où un objet est créé et celui où il est détruit!

Par analogie, réfléchissez de la façon suivante: pendant la vie d'une personne, celle-ci résidera dans de nombreux endroits différents, chacun avec une adresse distincte. Une durée de vie Rust concerne l'adresse que vous réside actuellement à , et non pas chaque fois que vous mourrez à l'avenir (bien que mourir change également votre adresse). Chaque fois que vous vous déplacez, c'est pertinent parce que votre adresse n'est plus valide.

Il est également important de noter que les durées de vie ne pas changent votre code; votre code contrôle la durée de vie, votre durée de vie ne contrôle pas le code. Le dicton pithy est "les durées de vie sont descriptives, pas normatives".

Annotons Combined::new Avec des numéros de ligne que nous utiliserons pour mettre en évidence les durées de vie:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

Le durée de vie concrète de parent va de 1 à 4 inclus (que je vais représenter par [1,4]). La durée de vie concrète de child est de [2,4] Et la durée de vie concrète de la valeur de retour est de [4,5]. Il est possible d'avoir des durées de vie concrètes qui commencent à zéro, ce qui représenterait la durée de vie d'un paramètre pour une fonction ou quelque chose qui existait en dehors du bloc.

Notez que la durée de vie de child elle-même est [2,4], Mais qu'elle fait référence à une valeur avec une durée de vie de [1,4]. Ceci est correct tant que la valeur de référence devient invalide avant la valeur de référence. Le problème se produit lorsque nous essayons de renvoyer child du bloc. Cela prolongerait la durée de vie au-delà de sa durée naturelle.

Cette nouvelle connaissance devrait expliquer les deux premiers exemples. Le troisième nécessite de regarder la mise en œuvre de Parent::child. Les chances sont, cela ressemblera à quelque chose comme ça:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

Ceci utilise durée de vie pour éviter d'écrire explicitement paramètres de durée de vie génériques . Cela équivaut à:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

Dans les deux cas, la méthode indique qu'une structure Child sera retournée et paramétrée avec la durée de vie concrète de self. En d'autres termes, l'instance Child contient une référence au Parent qui l'a créée et ne peut donc pas vivre plus longtemps que cette instance Parent.

Cela nous laisse également reconnaître que quelque chose ne va vraiment pas avec notre fonction de création:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

Bien que vous ayez plus de chances de le voir écrit sous une forme différente:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

Dans les deux cas, aucun paramètre de durée de vie n'est fourni via un argument. Cela signifie que la durée de vie avec laquelle Combined sera paramétrée n’est contrainte par rien - elle peut correspondre à ce que l’appelant souhaite que ce soit. Cela n'a pas de sens, car l'appelant pourrait spécifier la durée de vie 'static Et il n'y a aucun moyen de remplir cette condition.

Comment je le répare?

La solution la plus simple et la plus recommandée est de ne pas essayer de regrouper ces éléments dans la même structure. En procédant ainsi, votre structure imbriquée imitera la durée de vie de votre code. Placez les types qui possèdent des données dans une structure, puis fournissez des méthodes vous permettant d'obtenir des références ou des objets contenant des références, le cas échéant.

Il existe un cas particulier où le suivi de la durée de vie est trop zélé: lorsque vous avez quelque chose placé sur le tas. Cela se produit lorsque vous utilisez un Box<T>, Par exemple. Dans ce cas, la structure déplacée contient un pointeur dans le segment de mémoire. La valeur pointée restera stable, mais l'adresse du pointeur lui-même sera déplacée. En pratique, cela n'a pas d'importance, car vous suivez toujours le pointeur.

Le caisse de location ou le owning_ref caisse sont des moyens de représenter ce cas, mais ils nécessitent que l'adresse de base ne se déplace jamais . Cela exclut les vecteurs en mutation, ce qui peut provoquer une réallocation et un déplacement des valeurs attribuées au segment de mémoire.

Exemples de problèmes résolus avec Location:

Dans d'autres cas, vous voudrez peut-être passer à un type de comptage de références, tel que Rc ou Arc .

Plus d'information

Après avoir déplacé parent dans la structure, pourquoi le compilateur ne peut-il pas obtenir une nouvelle référence à parent et l'affecter à child dans la structure?

Cela est théoriquement possible, mais cela introduirait une grande complexité et des frais généraux excessifs. Chaque fois que l'objet est déplacé, le compilateur doit insérer du code pour "corriger" la référence. Cela signifierait que copier une structure n'est plus une opération très économique qui ne fait que déplacer des bits. Cela pourrait même signifier qu'un tel code coûte cher, en fonction de la qualité d'un optimiseur hypothétique:

let a = Object::new();
let b = a;
let c = b;

Au lieu de forcer cela pour every déplacer, le programmeur obtient choisissez quand cela se produira en créant des méthodes qui prendront le références appropriées uniquement lorsque vous les appelez.

Un type avec une référence à lui-même

Il existe un cas spécifique dans lequel vous pouvez créez un type avec une référence à lui-même. Vous devez utiliser quelque chose comme Option pour le faire en deux étapes:

#[derive(Debug)]
struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

fn main() {
    let mut tricky = WhatAboutThis {
        name: "Annabelle".to_string(),
        nickname: None,
    };
    tricky.nickname = Some(&tricky.name[..4]);

    println!("{:?}", tricky);
}

Cela fonctionne, dans un certain sens, mais la valeur créée est très limitée - elle peut never être déplacée. Cela signifie notamment qu'il ne peut pas être renvoyé par une fonction ou transmis par valeur à quoi que ce soit. Une fonction constructeur montre le même problème avec les durées de vie que ci-dessus:

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

Qu'en est-il de Pin?

Pin , stabilisé dans Rust 1.33, a ceci dans la documentation du module :

Un bon exemple d'un tel scénario serait la construction de structures auto-référentielles, car déplacer un objet avec des pointeurs sur lui-même les invaliderait, ce qui pourrait provoquer un comportement indéfini.

Il est important de noter que "auto-référentiel" ne signifie pas nécessairement utiliser une référence . En effet, le exemple d'une structure auto-référentielle dit spécifiquement (l'emphase mienne):

Nous ne pouvons pas en informer le compilateur avec une référence normale, car ce modèle ne peut pas être décrit avec les règles d'emprunt habituelles. Au lieu de cela , nous utilisons un pointeur brut , bien que celui-ci soit connu pour ne pas être nul, puisque nous savons qu'il pointe vers la chaîne.

La possibilité d'utiliser un pointeur brut pour ce comportement existe depuis Rust 1.0. En effet, la propriété-référence et la location utilisent des indicateurs bruts sous le capot.

La seule chose que Pin ajoute à la table est un moyen courant d'indiquer qu'une valeur donnée est garantie de ne pas être déplacée.

Voir également:

198
Shepmaster

Un problème légèrement différent qui provoque des messages de compilateur très similaires est la dépendance à la durée de vie des objets, plutôt que le stockage d'une référence explicite. Un exemple de cela est la bibliothèque ssh2 . Lors du développement de quelque chose de plus grand qu'un projet de test, il est tentant d'essayer de mettre côte-à-côte les Session et Channel obtenus dans cette session dans une structure, en masquant les détails d'implémentation à l'utilisateur. Cependant, notez que la définition de Channel a la durée de vie 'sess Dans son annotation de type, tandis que Session ne .

Cela provoque des erreurs de compilation similaires liées aux durées de vie.

Une façon très simple de le résoudre consiste à déclarer le Session à l'extérieur de l'appelant, puis à annoter la référence dans la structure avec une durée de vie, similaire à la réponse dans this Rust Post sur le forum de l'utilisateur parler du même problème lors de l'encapsulation de SFTP. Cela n'aura pas l'air élégant et ne s'appliquera peut-être pas toujours, car vous devez maintenant traiter avec deux entités plutôt que celle que vous souhaitiez!

Il s’avère que le caisse de location ou le propriété de caisse de l’autre réponse sont également les solutions à ce problème. Considérons le owning_ref, qui a un objet spécial dans ce but précis: OwningHandle . Pour éviter que l'objet sous-jacent ne se déplace, nous l'allouons sur le tas à l'aide de Box, ce qui nous donne la solution suivante:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.Shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Le résultat de ce code est que nous ne pouvons plus utiliser le Session, mais il est stocké à côté du Channel que nous allons utiliser. Parce que l'objet OwningHandle déréférence à Box, ce qui déréférence à Channel, lorsque vous le stockez dans une structure, nous l'appelons comme tel. REMARQUE: Ceci n’est que ma compréhension. Je soupçonne que cela peut ne pas être correct, car il semble être assez proche de discussion de OwningHandle insécurité) .

Un détail curieux ici est que le Session a logiquement une relation similaire avec TcpStream comme Channel à Session, mais sa propriété n’est pas prise et il y a aucune annotation de type autour de le faire. Au lieu de cela, il appartient à l'utilisateur de s'en occuper, comme indiqué dans la documentation de la méthode poignée de main :

Cette session ne prend pas possession du socket fourni, il est recommandé de s’assurer que le socket conserve sa durée de vie afin de s’assurer que la communication est correctement établie.

Il est également fortement recommandé que le flux fourni ne soit pas utilisé simultanément ailleurs pendant la durée de la session car il pourrait interférer avec le protocole.

Donc, avec l'utilisation de TcpStream, le programmeur est tout à fait libre de s'assurer de l'exactitude du code. Avec OwningHandle, l'attention est attirée sur l'endroit où se produit la "magie dangereuse" à l'aide du bloc unsafe {}.

Une autre discussion plus approfondie de ce problème se trouve dans ce fil du forum de l'utilisateur de Rust - qui inclut un exemple différent et sa solution utilisant la caisse de location, qui ne contient pas de blocs dangereux.

3
Andrew Y